mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 21:01:29 +00:00
* Implement spell AI pulling, fix throw stone * more pull tweaks * holding check at start of ai process * fully implement ^pull logic to always return, can still be overidden by ^attack * Rewrite ^pull logic and handling. **MORE** Add ^setassistee command to set who your bots will assist. Bots will always assist you first before anyone else. If the rule Bots, AllowCrossGroupRaidAssist is enabled bots will assist the group or raid main assists. Rewrites logic in handling of pull and returning to ensure bots make it back to their location. * Move HateLine to a better ID * cleanup ST_Self logic in CastChecks * Removed unused BotSpellTypeRequiresLoS * Move fizzle message to define * add timer checks to Idle/Engaged/Pursue CastCheck to early terminate * Add back !IsBotNonSpellFighter() check to the different CastCheck * Correct IsValidSpellRange * Implement AAs and harmtouch/layonhands to ^cast --- fix IsValidSpellRange * Add PetDamageShields and PetResistBuffs to IsPetBotSpellType() * Add priorities to HateLine inserts for db update * Remove SpellTypeRequiresCastChecks * Add bot check to DetermineSpellTargets for IsIllusionSpell * merge with previous * Correct bot checks for ST_GroupClientAndPet * Remove misc target_type checks * Add lull/aelull to ^cast * Add more checks for CommandedSubTypes::AETarget * remove unneeded checks on IsValidSpellTypeBySpellID * add to aelull * rewrite GetCorrectSpellType * Add IsBlockedBuff to CastChecks * Add spellid option to ^cast to allow casting of a specific spell by ID * ^cast adjustments for spellid casts * Add missing alert round for ranged attacks * More castcheck improvements * CanUseBotSpell for ^cast * remove ht/loh from attack ai * remove SetCombatRoundForAlerts that triggered every engagement * Add RangedAttackImmunity checks before trying to ranged attack * move bot backstab to mob * fix MinStatusToBypassCreateLimit * more backstab to mob cleanup * add bot checks to tryheadshot / tryassassinate * adjust version number for bots * add back m_mob_check_moving_timer, necessary? * add sanity checks for classattacks * Get rid of Bots:BotGroupXP and change logic to support Bots:SameRaidGroupForXP Bots won't do anything if not in the same group so this should more accurately control only when in the same raid group. * add "confirm" check to ^delete * Update bot.cpp * Remove `id` from bot_settings, correct types * Implement blocked_buffs and blocked_pet_buffs * more blocked buff tweaks * add beneficial check to ^blockedbuffs * command grammar * missing ) * Move getnames for categories and settings to mob, rename hptomed/manatomed * add GetBotSpellCategoryIDByShortName and CopyBotBlockedPetBuffs, update ^defaultsettings command * cls cleanup * Allow bots to clear HasProjectIllusion flag * Add PercentChanceToCastGroupCure * Implmenet PetCures, add some missing types for defaults/chance to cast * Change GetRaidByBotName to GetRaidByBot * Typo on PetBuffs implement * Change GetSpellListSpellType to GetParentSpellType * missing from GetChanceToCastBySpellType * Fix performance in IsValidSpellRange by flipping HasProjectIllusion * merge with prev * merge with cls cleanup * Reorder IsTargetAlreadyReceivingSpell/CheckSpellLevelRestriction/IsBlockedBuff * Combine GatherGroupSpellTargets and GatherSpellTargets * Cleanup IsTargetAlreadyReceivingSpell * Fix ^petsettype to account for usable levels of spells and remove hardcoded level limits. * Remove Bot_AICheckCloseBeneficialSpells and use AttemptCloseBeneficialSpells for better performance * remove default hold for resist buffa * move IsValidSpellRange further down castchecks * raid optimizations * correct name checking to match players * more name checks and add proper soft deletes to bots * organize some checks in IsImmuneToBotSpell * Fix GetRaidByBotName and GetRaidByBot checks to not loop unnecessarily * Move GatherSpellTargets to mob * Change GetPrioritizedBotSpellsBySpellType to vector Some slipped through in "organize some checks in IsImmuneToBotSpell" * Move GatherSpellTargets and Raid to stored variables. Missing some in "organize some checks in IsImmuneToBotSpell" * comment out precheck, delays, thresholds, etc logging missed some in "organize some checks in IsImmuneToBotSpell" * Missing IsInGroupOrRaid cleanup * Implement AIBot_spells_by_type to reduce looping when searching for spells * Add _tempSpellType as placeholder for any future passthru * todo * Move bot_list from std::list to std::unordered_map like other entities * Fix missing raid assignment for GetStoredRaid in IsInGroupOrRaid * TempPet owned by bots that get the kill will now give exp like a client would * Remove unnecessary checks in bot process (closescanmoving timer, verify raid, send hp/mana/end packet * Fix client spell commands from saving the wrong setting * Cleanup ^copysettings command and add new commands * Add pet option to ^taunt No longer has toggle, required on/off option and an optional "pet" option to control pets' taunting state * Allow pet types to ^cast, prevent failure spam, add cure check * more raid optimizations, should be final. 10 clients, 710 bots, 10 raids, ~250 pets sits around 3.5% CPU idle * Move spell range check to proper location * Implement ^discipline * remove ^aggressive/^defensive * remove this for a separate PR * cleanup * Add BotGroupSay method * todo list * Add missing bot_blocked_buffs to schema * Remove plural on ^spelltypeidsand ^spelltypenames * Move spelltype names, spell subtypes, category names and setting names to maps. * move los checks to mob.cpp * Bot CampAll fix * Bots special_attacks.cpp fix * Add zero check for bot spawn limits If the spawn limit rule is set to 0 and spawn limit is set by bucket, if no class buckets are set, it defaults to the rule of 0 and renders the player unable to spawn bots. This adds a check where if the rule and class bucket are 0, it will check for the spawn limit bucket * Add HasSkill checks to bot special abilities (kick/bash/etc) * code cleanup 1 * code cleanup 2 * code cleanup 3 * code cleanup 4 * fix ^cast wirh commanded types * Remove bcspells, fix helper_send_usage_required_bots * linux build fix * remove completed todo * Allow inventory give to specific ID slots * Update TODO * Correct slot ranges for inventorygive * Add zone specific spawn limits and zone specific forced spawn limits * remove bd. from update queries where it doesn't exist * Rename _spellSettings to m_bot_spell_settings * Add IsPetOwnerOfClientBot(), add Lua and Perl methods * Make botOwnerCharacterID snakecase * Throw bot_camp_timer behind Bots:Enabled rule * Move various Bot<>Checks logging to BotSpellChecks * Remove from LogCategoryName * Consolidate IsInGroupOrRaid * Consolidate GatherSpellTargets * Add missing Bot Spell Type Checks to log * Add GetParentSpellType when checking spelltypes for idle, engaged, pursue CastChecks. * Consolidate AttemptForcedCastSpell * Consolidate SetBotBlockedBuff/SetBotBlockedPetBuff * Add list option to ^spellpriority commands. * Move client functions to client_bot * Move mob functions to mob_bot * Move bot spdat functions to spdat_bot * Move SendCommandHelpWindow to SendBotCommandHelpWindow and simplify * Change char_id to character_id for bot_settings * update todo * Fix typo on merge conflict * Cleanup command format changes, remove hardcoded class IDs in examples. * Set #illusionblock for players to guide access * Move client commands for bot spells from gm commands to existing bot commands * Fix alignment issues * More alignment fixes * More cleanup 1 * More cleanup 2 * Fix BotMeditate to med at proper percentages * Correct GetStopMeleeLevel checks for some buff checks * Add back hpmanaend update to bot raid, force timer update to prevent spamming * Remove log * Cleanup ranged and ammo calculations - Adds throwing check for match * Add check in distance calculations to stay at range if set even if no ammo or ranged * Move melee distance calculations to better function * Add GetBuffTargets helper * Missing p_item, s_item in CombatRangeInput * Linux test? * Reduce GetCorrectBotSpellType branching slightly This is still an ugly ass function but my brain is melted * Line fixes * Make bot pets only do half damage in pvp * Add bot pet pvp damage to tune * Add bot pet check for AIYellForHelp * Add bots to UseSpellImpliedTargeting * Move toggleranged, togglehelm and illusionblock to new help window. Add actionable support * Add bot and bot pet checks to various spells, auras and targeting checks that were missing. * update todo * New lines * Correct DoLosChecks * Remove Log TestDebug * Remove _Struct from struct declarations * Add bot check to IsAttackAllowed for GetUltimateOwner to skip entity list where possible * Wrap SaveBotSettings in Bots Enabled check * Remove comment * Wrap bot setting loading for clients in bots enabled rule * Cleanup BlockedBuffs logic in SpellOnTarget * Rename BotSpells_Struct/BotSpells_Struct_wIndex * Rename spawn/create status bypass rules, fix return for spawn limit * Remove unnecessary return in CanBuffStack, cleanup * Enable recastdelay support for clients * Remove unused variables * Rename _assistee to bot_assistee * hardcode BotCommandHelpWindow colors * todo * Fix ^cast summoncorpse * todo * Reimplement secondary colors to BotSendCommandHelpWindow * Give ^copysettings/^defaultsettings more options, cleanup. * Cleanup some commands * Add comment to CheckLosCheat/CheckLosCheatExempt * Make struct BotSpellSettings snake case * Allow duplicate casts of same spell on target for heals and cures * Add default delay to cures * Remove unused methods * Implement missing ^spellresistlimits/^resistlimits command * Move functions out of mob.h and cleanup * Return for GetRawBotList This checks offline bots too * Rename BotGroupSay to RaidGroupSay * Prevent bots from forming their own group if a bot that is a group leader is removed from the raid * Linux fix? * IsPetOwner fixes * Add remove option to list for ^blockedbuffs / ^blockedpetbuffs * Implement ^spellannouncecasts to toggle announcing casts of spell types * Remove rule Bots:BardsAnnounceCasts * Update bot.h * Remove unused no_pets option from GatherSpellTargets * Move ^attack response back to normal chat window (other) * Set lower limit of spell delays to 100 rather than 1 * Correct pet checks on GetUltimateSpell functions * Add rules (Bots, AICastSpellTypeDelay, Bots, AICastSpellTypeHeldDelay) to prevent spamming of failed spell type AI casts * Correct pet buff type logic to catch DS/Resists with other spell effects in them * Fix defaults for clients * Add more logic for necros/shaman for default heal thresholds due to lich and canni * Rename SpellHold, SpellDelay, SpellMinThreshold, SpellMaxThreshold, SpellRecastDelay to fit SpellType style naming * Use GetTempSpellType() for announce check in RaidGroupSay * Make all spell shortnames plural where applicable * Update bot.cpp * Bots:BotsUseLiveBlockedMessage filter to spell failure * Move GetSpellTargetList to only get called when necessary to reduce overhead * formatting * Formatting * Simplify case SE_Illusion and SE_IllusionCopy for GetIllusionBlock * Clean up InterruptSpell * Cleanup IsBot() checks for DetermineSpellTargets->ST_GroupClientAndPet * Cleanup range/aoe_range check in SpellFinished * Cleanup DetermineSpellTargets->ST_GroupNoPets * Cleanup DetermineSpellTargets->ST_Self for bot summon corpse * Cleanup DetermineSpellTargets->ST_Pet * Cleanup bot logic in TryBackstab * Cleanup IsAttackAllowed checks for bots and their pets * Cleanup StopMoving for bots * Cleanup CanThisClassTripleAttack * Fix casting for GetIllusionBlock checks * Formatting * Fix DetermineSpellTargets for group spells (this also wasn't properly checking the rule Character:EnableTGB in master) * Cleanup spelltarget grabbing logic, consolidate group heals in to GetNumberNeedingHealedInGroup * Throw added client los pet checks behind LoS cheat rule for bots * CLeanup give_exp on npc death logic and ensure client pets always pass. * Undo unintended rename from previous refactor * Remove pointless Bots, SameRaidGroupForXP rule * Revision to 0690783a9d1e99005d6bee0824597ea920e26df9 --------- Co-authored-by: Akkadius <akkadius1@gmail.com>
5924 lines
155 KiB
C++
5924 lines
155 KiB
C++
|
|
#include "merc.h"
|
|
#include "client.h"
|
|
#include "corpse.h"
|
|
#include "entity.h"
|
|
#include "groups.h"
|
|
#include "mob.h"
|
|
#include "quest_parser_collection.h"
|
|
|
|
#include "zone.h"
|
|
#include "string_ids.h"
|
|
#include "../common/skill_caps.h"
|
|
|
|
extern volatile bool is_zone_loaded;
|
|
|
|
#if EQDEBUG >= 12
|
|
#define MercAI_DEBUG_Spells 25
|
|
#elif EQDEBUG >= 9
|
|
#define MercAI_DEBUG_Spells 10
|
|
#else
|
|
#define MercAI_DEBUG_Spells -1
|
|
#endif
|
|
|
|
Merc::Merc(const NPCType* d, float x, float y, float z, float heading)
|
|
: NPC(d, nullptr, glm::vec4(x, y, z, heading), GravityBehavior::Water, false), endupkeep_timer(1000), rest_timer(1), confidence_timer(6000), check_target_timer(2000)
|
|
{
|
|
base_hp = d->max_hp;
|
|
base_mana = d->Mana;
|
|
_baseAC = d->AC;
|
|
_baseSTR = d->STR;
|
|
_baseSTA = d->STA;
|
|
_baseDEX = d->DEX;
|
|
_baseAGI = d->AGI;
|
|
_baseINT = d->INT;
|
|
_baseWIS = d->WIS;
|
|
_baseCHA = d->CHA;
|
|
_baseATK = d->ATK;
|
|
_baseRace = d->race;
|
|
_baseGender = d->gender;
|
|
_baseMR = d->MR;
|
|
_baseCR = d->CR;
|
|
_baseDR = d->DR;
|
|
_baseFR = d->FR;
|
|
_basePR = d->PR;
|
|
_baseCorrup = d->Corrup;
|
|
_OwnerClientVersion = static_cast<unsigned int>(EQ::versions::ClientVersion::Titanium);
|
|
RestRegenHP = 0;
|
|
RestRegenMana = 0;
|
|
RestRegenEndurance = 0;
|
|
cur_end = 0;
|
|
|
|
_medding = false;
|
|
_suspended = false;
|
|
p_depop = false;
|
|
_check_confidence = false;
|
|
_lost_confidence = false;
|
|
_hatedCount = 0;
|
|
|
|
memset(equipment, 0, sizeof(equipment));
|
|
|
|
SetMercID(0);
|
|
SetStance(Stance::Balanced);
|
|
rest_timer.Disable();
|
|
|
|
if (GetClass() == Class::Rogue)
|
|
evade_timer.Start();
|
|
|
|
int r;
|
|
for (r = 0; r <= EQ::skills::HIGHEST_SKILL; r++) {
|
|
skills[r] = skill_caps.GetSkillCap(GetClass(), (EQ::skills::SkillType)r, GetLevel()).cap;
|
|
}
|
|
|
|
size = d->size;
|
|
CalcBonuses();
|
|
|
|
// Class should use npc constructor to set light properties
|
|
|
|
RestoreHealth();
|
|
RestoreMana();
|
|
RestoreEndurance();
|
|
|
|
AI_Start();
|
|
}
|
|
|
|
Merc::~Merc() {
|
|
AI_Stop();
|
|
//entity_list.RemoveMerc(GetID());
|
|
UninitializeBuffSlots();
|
|
}
|
|
|
|
void Merc::CalcBonuses()
|
|
{
|
|
memset(&itembonuses, 0, sizeof(StatBonuses));
|
|
memset(&aabonuses, 0, sizeof(StatBonuses));
|
|
CalcItemBonuses(&itembonuses);
|
|
|
|
CalcSpellBonuses(&spellbonuses);
|
|
|
|
CalcAC();
|
|
CalcATK();
|
|
|
|
CalcSTR();
|
|
CalcSTA();
|
|
CalcDEX();
|
|
CalcAGI();
|
|
CalcINT();
|
|
CalcWIS();
|
|
CalcCHA();
|
|
|
|
CalcMR();
|
|
CalcFR();
|
|
CalcDR();
|
|
CalcPR();
|
|
CalcCR();
|
|
CalcCorrup();
|
|
|
|
CalcMaxHP();
|
|
CalcMaxMana();
|
|
CalcMaxEndurance();
|
|
|
|
rooted = FindType(SE_Root);
|
|
}
|
|
|
|
float Merc::GetDefaultSize() {
|
|
|
|
float MercSize = GetSize();
|
|
|
|
switch(GetRace())
|
|
{
|
|
case 1: // Humans
|
|
MercSize = 6.0;
|
|
break;
|
|
case 2: // Barbarian
|
|
MercSize = 7.0;
|
|
break;
|
|
case 3: // Erudite
|
|
MercSize = 6.0;
|
|
break;
|
|
case 4: // Wood Elf
|
|
MercSize = 5.0;
|
|
break;
|
|
case 5: // High Elf
|
|
MercSize = 6.0;
|
|
break;
|
|
case 6: // Dark Elf
|
|
MercSize = 5.0;
|
|
break;
|
|
case 7: // Half Elf
|
|
MercSize = 5.5;
|
|
break;
|
|
case 8: // Dwarf
|
|
MercSize = 4.0;
|
|
break;
|
|
case 9: // Troll
|
|
MercSize = 8.0;
|
|
break;
|
|
case 10: // Ogre
|
|
MercSize = 9.0;
|
|
break;
|
|
case 11: // Halfling
|
|
MercSize = 3.5;
|
|
break;
|
|
case 12: // Gnome
|
|
MercSize = 3.0;
|
|
break;
|
|
case 128: // Iksar
|
|
MercSize = 6.0;
|
|
break;
|
|
case 130: // Vah Shir
|
|
MercSize = 7.0;
|
|
break;
|
|
case 330: // Froglok
|
|
MercSize = 5.0;
|
|
break;
|
|
case 522: // Drakkin
|
|
MercSize = 5.0;
|
|
break;
|
|
default:
|
|
MercSize = 6.0;
|
|
break;
|
|
}
|
|
|
|
return MercSize;
|
|
}
|
|
|
|
int Merc::GroupLeadershipAAHealthEnhancement()
|
|
{
|
|
Group *g = GetGroup();
|
|
|
|
if(!g || (g->GroupCount() < 3))
|
|
return 0;
|
|
|
|
switch(g->GetLeadershipAA(groupAAHealthEnhancement))
|
|
{
|
|
case 0:
|
|
return 0;
|
|
case 1:
|
|
return 30;
|
|
case 2:
|
|
return 60;
|
|
case 3:
|
|
return 100;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int Merc::GroupLeadershipAAManaEnhancement()
|
|
{
|
|
Group *g = GetGroup();
|
|
|
|
if(!g || (g->GroupCount() < 3))
|
|
return 0;
|
|
|
|
switch(g->GetLeadershipAA(groupAAManaEnhancement))
|
|
{
|
|
case 0:
|
|
return 0;
|
|
case 1:
|
|
return 30;
|
|
case 2:
|
|
return 60;
|
|
case 3:
|
|
return 100;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int Merc::GroupLeadershipAAHealthRegeneration()
|
|
{
|
|
Group *g = GetGroup();
|
|
|
|
if(!g || (g->GroupCount() < 3))
|
|
return 0;
|
|
|
|
switch(g->GetLeadershipAA(groupAAHealthRegeneration))
|
|
{
|
|
case 0:
|
|
return 0;
|
|
case 1:
|
|
return 4;
|
|
case 2:
|
|
return 6;
|
|
case 3:
|
|
return 8;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int Merc::GroupLeadershipAAOffenseEnhancement()
|
|
{
|
|
Group *g = GetGroup();
|
|
|
|
if(!g || (g->GroupCount() < 3))
|
|
return 0;
|
|
|
|
switch(g->GetLeadershipAA(groupAAOffenseEnhancement))
|
|
{
|
|
case 0:
|
|
return 0;
|
|
case 1:
|
|
return 10;
|
|
case 2:
|
|
return 19;
|
|
case 3:
|
|
return 28;
|
|
case 4:
|
|
return 34;
|
|
case 5:
|
|
return 40;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int32 Merc::CalcSTR() {
|
|
int32 val = _baseSTR + itembonuses.STR + spellbonuses.STR;
|
|
|
|
int32 mod = aabonuses.STR;
|
|
|
|
STR = val + mod;
|
|
|
|
if(STR < 1)
|
|
STR = 1;
|
|
|
|
return(STR);
|
|
}
|
|
|
|
int32 Merc::CalcSTA() {
|
|
int32 val = _baseSTA + itembonuses.STA + spellbonuses.STA;
|
|
|
|
int32 mod = aabonuses.STA;
|
|
|
|
STA = val + mod;
|
|
|
|
if(STA < 1)
|
|
STA = 1;
|
|
|
|
return(STA);
|
|
}
|
|
|
|
int32 Merc::CalcAGI() {
|
|
int32 val = _baseAGI + itembonuses.AGI + spellbonuses.AGI;
|
|
int32 mod = aabonuses.AGI;
|
|
|
|
int32 str = GetSTR();
|
|
|
|
AGI = val + mod;
|
|
|
|
if(AGI < 1)
|
|
AGI = 1;
|
|
|
|
return(AGI);
|
|
}
|
|
|
|
int32 Merc::CalcDEX() {
|
|
int32 val = _baseDEX + itembonuses.DEX + spellbonuses.DEX;
|
|
|
|
int32 mod = aabonuses.DEX;
|
|
|
|
DEX = val + mod;
|
|
|
|
if(DEX < 1)
|
|
DEX = 1;
|
|
|
|
return(DEX);
|
|
}
|
|
|
|
int32 Merc::CalcINT() {
|
|
int32 val = _baseINT + itembonuses.INT + spellbonuses.INT;
|
|
|
|
int32 mod = aabonuses.INT;
|
|
|
|
INT = val + mod;
|
|
|
|
if(INT < 1)
|
|
INT = 1;
|
|
|
|
return(INT);
|
|
}
|
|
|
|
int32 Merc::CalcWIS() {
|
|
int32 val = _baseWIS + itembonuses.WIS + spellbonuses.WIS;
|
|
|
|
int32 mod = aabonuses.WIS;
|
|
|
|
WIS = val + mod;
|
|
|
|
if(WIS < 1)
|
|
WIS = 1;
|
|
|
|
return(WIS);
|
|
}
|
|
|
|
int32 Merc::CalcCHA() {
|
|
int32 val = _baseCHA + itembonuses.CHA + spellbonuses.CHA;
|
|
|
|
int32 mod = aabonuses.CHA;
|
|
|
|
CHA = val + mod;
|
|
|
|
if(CHA < 1)
|
|
CHA = 1;
|
|
|
|
return(CHA);
|
|
}
|
|
|
|
//The AA multipliers are set to be 5, but were 2 on WR
|
|
//The resistant discipline which I think should be here is implemented
|
|
//in Mob::ResistSpell
|
|
int32 Merc::CalcMR()
|
|
{
|
|
MR = _baseMR + itembonuses.MR + spellbonuses.MR + aabonuses.MR;
|
|
|
|
if(MR < 1)
|
|
MR = 1;
|
|
|
|
return(MR);
|
|
}
|
|
|
|
int32 Merc::CalcFR()
|
|
{
|
|
FR = _baseFR + itembonuses.FR + spellbonuses.FR + aabonuses.FR;
|
|
|
|
if(FR < 1)
|
|
FR = 1;
|
|
|
|
return(FR);
|
|
}
|
|
|
|
int32 Merc::CalcDR()
|
|
{
|
|
DR = _baseDR + itembonuses.DR + spellbonuses.DR + aabonuses.DR;
|
|
|
|
if(DR < 1)
|
|
DR = 1;
|
|
|
|
return(DR);
|
|
}
|
|
|
|
int32 Merc::CalcPR()
|
|
{
|
|
PR = _basePR + itembonuses.PR + spellbonuses.PR + aabonuses.PR;
|
|
|
|
if(PR < 1)
|
|
PR = 1;
|
|
|
|
return(PR);
|
|
}
|
|
|
|
int32 Merc::CalcCR()
|
|
{
|
|
CR = _baseCR + itembonuses.CR + spellbonuses.CR + aabonuses.CR;
|
|
|
|
if(CR < 1)
|
|
CR = 1;
|
|
|
|
return(CR);
|
|
}
|
|
|
|
int32 Merc::CalcCorrup()
|
|
{
|
|
Corrup = _baseCorrup + itembonuses.Corrup + spellbonuses.Corrup + aabonuses.Corrup;
|
|
|
|
return(Corrup);
|
|
}
|
|
|
|
int32 Merc::CalcATK() {
|
|
ATK = _baseATK + itembonuses.ATK + spellbonuses.ATK + aabonuses.ATK + GroupLeadershipAAOffenseEnhancement();
|
|
return(ATK);
|
|
}
|
|
|
|
int32 Merc::CalcAC() {
|
|
//spell AC bonuses are added directly to natural total
|
|
AC = _baseAC + spellbonuses.AC;
|
|
return(AC);
|
|
}
|
|
|
|
int64 Merc::CalcHPRegen() {
|
|
int64 regen = hp_regen + itembonuses.HPRegen + spellbonuses.HPRegen;
|
|
|
|
regen += aabonuses.HPRegen + GroupLeadershipAAHealthRegeneration();
|
|
|
|
return (regen * RuleI(Character, HPRegenMultiplier) / 100);
|
|
}
|
|
|
|
int64 Merc::CalcHPRegenCap()
|
|
{
|
|
int cap = RuleI(Character, ItemHealthRegenCap) + itembonuses.HeroicSTA/25;
|
|
|
|
cap += aabonuses.ItemHPRegenCap + spellbonuses.ItemHPRegenCap + itembonuses.ItemHPRegenCap;
|
|
|
|
return (cap * RuleI(Character, HPRegenMultiplier) / 100);
|
|
}
|
|
|
|
int64 Merc::CalcMaxHP() {
|
|
float nd = 10000;
|
|
max_hp = (CalcBaseHP() + itembonuses.HP);
|
|
|
|
//The AA desc clearly says it only applies to base hp..
|
|
//but the actual effect sent on live causes the client
|
|
//to apply it to (basehp + itemhp).. I will oblige to the client's whims over
|
|
//the aa description
|
|
nd += aabonuses.PercentMaxHPChange + spellbonuses.PercentMaxHPChange + itembonuses.PercentMaxHPChange; //Natural Durability, Physical Enhancement, Planar Durability
|
|
|
|
max_hp = (float)max_hp * (float)nd / (float)10000; //this is to fix the HP-above-495k issue
|
|
max_hp += spellbonuses.FlatMaxHPChange + aabonuses.FlatMaxHPChange + itembonuses.FlatMaxHPChange;
|
|
|
|
max_hp += GroupLeadershipAAHealthEnhancement();
|
|
|
|
if (current_hp > max_hp)
|
|
current_hp = max_hp;
|
|
|
|
int64 hp_perc_cap = spellbonuses.HPPercCap[SBIndex::RESOURCE_PERCENT_CAP];
|
|
if(hp_perc_cap) {
|
|
int64 curHP_cap = (max_hp * hp_perc_cap) / 100;
|
|
if (current_hp > curHP_cap || (spellbonuses.HPPercCap[SBIndex::RESOURCE_AMOUNT_CAP] && current_hp > spellbonuses.HPPercCap[SBIndex::RESOURCE_AMOUNT_CAP]))
|
|
current_hp = curHP_cap;
|
|
}
|
|
|
|
return max_hp;
|
|
}
|
|
|
|
int64 Merc::CalcBaseHP()
|
|
{
|
|
return base_hp;
|
|
}
|
|
|
|
int64 Merc::CalcMaxMana()
|
|
{
|
|
if (IsIntelligenceCasterClass() || IsWisdomCasterClass()) {
|
|
max_mana = (CalcBaseMana() + itembonuses.Mana + spellbonuses.Mana + GroupLeadershipAAManaEnhancement());
|
|
} else {
|
|
max_mana = 0;
|
|
}
|
|
|
|
if (max_mana < 0) {
|
|
max_mana = 0;
|
|
}
|
|
|
|
if (current_mana > max_mana) {
|
|
current_mana = max_mana;
|
|
}
|
|
|
|
int mana_perc_cap = spellbonuses.ManaPercCap[SBIndex::RESOURCE_PERCENT_CAP];
|
|
if(mana_perc_cap) {
|
|
int curMana_cap = (max_mana * mana_perc_cap) / 100;
|
|
if (current_mana > curMana_cap || (spellbonuses.ManaPercCap[SBIndex::RESOURCE_AMOUNT_CAP] && current_mana > spellbonuses.ManaPercCap[SBIndex::RESOURCE_AMOUNT_CAP]))
|
|
current_mana = curMana_cap;
|
|
}
|
|
|
|
return max_mana;
|
|
}
|
|
|
|
int64 Merc::CalcBaseMana()
|
|
{
|
|
return base_mana;
|
|
}
|
|
|
|
int64 Merc::CalcBaseManaRegen()
|
|
{
|
|
uint8 clevel = GetLevel();
|
|
int32 regen = 0;
|
|
if (IsSitting())
|
|
{
|
|
if (HasSkill(EQ::skills::SkillMeditate))
|
|
regen = (((GetSkill(EQ::skills::SkillMeditate) / 10) + (clevel - (clevel / 4))) / 4) + 4;
|
|
else
|
|
regen = 2;
|
|
}
|
|
else {
|
|
regen = 2;
|
|
}
|
|
return regen;
|
|
}
|
|
|
|
int64 Merc::CalcManaRegen()
|
|
{
|
|
int64 regen = 0;
|
|
if (IsSitting())
|
|
{
|
|
BuffFadeBySitModifier();
|
|
if (HasSkill(EQ::skills::SkillMeditate)) {
|
|
_medding = true;
|
|
regen = ((GetSkill(EQ::skills::SkillMeditate) / 10) + mana_regen);
|
|
regen += spellbonuses.ManaRegen + itembonuses.ManaRegen;
|
|
}
|
|
else
|
|
regen = mana_regen + spellbonuses.ManaRegen + itembonuses.ManaRegen;
|
|
}
|
|
else {
|
|
_medding = false;
|
|
regen = mana_regen + spellbonuses.ManaRegen + itembonuses.ManaRegen;
|
|
}
|
|
|
|
if (IsIntelligenceCasterClass()) {
|
|
regen += (itembonuses.HeroicINT / 25);
|
|
} else if (IsWisdomCasterClass()) {
|
|
regen += (itembonuses.HeroicWIS / 25);
|
|
} else {
|
|
regen = 0;
|
|
}
|
|
|
|
//AAs
|
|
regen += aabonuses.ManaRegen;
|
|
|
|
return (regen * RuleI(Character, ManaRegenMultiplier) / 100);
|
|
}
|
|
|
|
int64 Merc::CalcManaRegenCap()
|
|
{
|
|
int64 cap = RuleI(Character, ItemManaRegenCap) + aabonuses.ItemManaRegenCap;
|
|
|
|
if (IsIntelligenceCasterClass()) {
|
|
cap += (itembonuses.HeroicINT / 25);
|
|
} else if (IsWisdomCasterClass()) {
|
|
cap += (itembonuses.HeroicWIS / 25);
|
|
}
|
|
|
|
return (cap * RuleI(Character, ManaRegenMultiplier) / 100);
|
|
}
|
|
|
|
void Merc::CalcMaxEndurance()
|
|
{
|
|
max_end = CalcBaseEndurance() + spellbonuses.Endurance + itembonuses.Endurance + aabonuses.Endurance;
|
|
|
|
if (max_end < 0) {
|
|
max_end = 0;
|
|
}
|
|
|
|
if (cur_end > max_end) {
|
|
cur_end = max_end;
|
|
}
|
|
|
|
int end_perc_cap = spellbonuses.EndPercCap[SBIndex::RESOURCE_PERCENT_CAP];
|
|
if(end_perc_cap) {
|
|
int curEnd_cap = (max_end * end_perc_cap) / 100;
|
|
if (cur_end > curEnd_cap || (spellbonuses.EndPercCap[SBIndex::RESOURCE_AMOUNT_CAP] && cur_end > spellbonuses.EndPercCap[SBIndex::RESOURCE_AMOUNT_CAP]))
|
|
cur_end = curEnd_cap;
|
|
}
|
|
}
|
|
|
|
int64 Merc::CalcBaseEndurance()
|
|
{
|
|
int64 base_end = 0;
|
|
int64 base_endurance = 0;
|
|
int32 ConvertedStats = 0;
|
|
int32 sta_end = 0;
|
|
int Stats = 0;
|
|
|
|
if (GetClientVersion() >= static_cast<unsigned int>(EQ::versions::ClientVersion::SoD) && RuleB(Character, SoDClientUseSoDHPManaEnd)) {
|
|
int HeroicStats = 0;
|
|
|
|
Stats = ((GetSTR() + GetSTA() + GetDEX() + GetAGI()) / 4);
|
|
HeroicStats = ((GetHeroicSTR() + GetHeroicSTA() + GetHeroicDEX() + GetHeroicAGI()) / 4);
|
|
|
|
if (Stats > 100) {
|
|
ConvertedStats = (((Stats - 100) * 5 / 2) + 100);
|
|
if (Stats > 201) {
|
|
ConvertedStats -= ((Stats - 201) * 5 / 4);
|
|
}
|
|
}
|
|
else {
|
|
ConvertedStats = Stats;
|
|
}
|
|
|
|
if (GetLevel() < 41) {
|
|
sta_end = (GetLevel() * 75 * ConvertedStats / 1000);
|
|
base_endurance = (GetLevel() * 15);
|
|
}
|
|
else if (GetLevel() < 81) {
|
|
sta_end = ((3 * ConvertedStats) + ((GetLevel() - 40) * 15 * ConvertedStats / 100));
|
|
base_endurance = (600 + ((GetLevel() - 40) * 30));
|
|
}
|
|
else {
|
|
sta_end = (9 * ConvertedStats);
|
|
base_endurance = (1800 + ((GetLevel() - 80) * 18));
|
|
}
|
|
base_end = (base_endurance + sta_end + (HeroicStats * 10));
|
|
}
|
|
else
|
|
{
|
|
Stats = GetSTR()+GetSTA()+GetDEX()+GetAGI();
|
|
int LevelBase = GetLevel() * 15;
|
|
|
|
int at_most_800 = Stats;
|
|
if(at_most_800 > 800)
|
|
at_most_800 = 800;
|
|
|
|
int Bonus400to800 = 0;
|
|
int HalfBonus400to800 = 0;
|
|
int Bonus800plus = 0;
|
|
int HalfBonus800plus = 0;
|
|
|
|
int BonusUpto800 = int( at_most_800 / 4 ) ;
|
|
if(Stats > 400) {
|
|
Bonus400to800 = int( (at_most_800 - 400) / 4 );
|
|
HalfBonus400to800 = int( std::max( ( at_most_800 - 400 ), 0 ) / 8 );
|
|
|
|
if(Stats > 800) {
|
|
Bonus800plus = int( (Stats - 800) / 8 ) * 2;
|
|
HalfBonus800plus = int( (Stats - 800) / 16 );
|
|
}
|
|
}
|
|
int bonus_sum = BonusUpto800 + Bonus400to800 + HalfBonus400to800 + Bonus800plus + HalfBonus800plus;
|
|
|
|
base_end = LevelBase;
|
|
|
|
//take all of the sums from above, then multiply by level*0.075
|
|
base_end += ( bonus_sum * 3 * GetLevel() ) / 40;
|
|
}
|
|
return base_end;
|
|
}
|
|
|
|
int64 Merc::CalcEnduranceRegen() {
|
|
int64 regen = int32(GetLevel() * 4 / 10) + 2;
|
|
regen += aabonuses.EnduranceRegen + spellbonuses.EnduranceRegen + itembonuses.EnduranceRegen;
|
|
|
|
return (regen * RuleI(Character, EnduranceRegenMultiplier) / 100);
|
|
}
|
|
|
|
int64 Merc::CalcEnduranceRegenCap() {
|
|
int64 cap = (RuleI(Character, ItemEnduranceRegenCap) + itembonuses.HeroicSTR/25 + itembonuses.HeroicDEX/25 + itembonuses.HeroicAGI/25 + itembonuses.HeroicSTA/25);
|
|
|
|
return (cap * RuleI(Character, EnduranceRegenMultiplier) / 100);
|
|
}
|
|
|
|
void Merc::SetEndurance(int32 newEnd)
|
|
{
|
|
/*Endurance can't be less than 0 or greater than max*/
|
|
if(newEnd < 0)
|
|
newEnd = 0;
|
|
else if(newEnd > GetMaxEndurance()){
|
|
newEnd = GetMaxEndurance();
|
|
}
|
|
|
|
cur_end = newEnd;
|
|
}
|
|
|
|
void Merc::DoEnduranceUpkeep() {
|
|
|
|
if (!HasEndurUpkeep())
|
|
return;
|
|
|
|
int upkeep_sum = 0;
|
|
int cost_redux = spellbonuses.EnduranceReduction + itembonuses.EnduranceReduction;
|
|
|
|
bool has_effect = false;
|
|
uint32 buffs_i;
|
|
uint32 buff_count = GetMaxTotalSlots();
|
|
for (buffs_i = 0; buffs_i < buff_count; buffs_i++) {
|
|
if (IsValidSpell(buffs[buffs_i].spellid)) {
|
|
int upkeep = spells[buffs[buffs_i].spellid].endurance_upkeep;
|
|
if(upkeep > 0) {
|
|
has_effect = true;
|
|
if(cost_redux > 0) {
|
|
if(upkeep <= cost_redux)
|
|
continue; //reduced to 0
|
|
upkeep -= cost_redux;
|
|
}
|
|
if((upkeep+upkeep_sum) > GetEndurance()) {
|
|
//they do not have enough to keep this one going.
|
|
BuffFadeBySlot(buffs_i);
|
|
} else {
|
|
upkeep_sum += upkeep;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(upkeep_sum != 0)
|
|
SetEndurance(GetEndurance() - upkeep_sum);
|
|
|
|
if (!has_effect)
|
|
SetEndurUpkeep(false);
|
|
}
|
|
|
|
void Merc::CalcRestState() {
|
|
|
|
// This method calculates rest state HP and mana regeneration.
|
|
// The bot must have been out of combat for RuleI(Character, RestRegenTimeToActivate) seconds,
|
|
// must be sitting down, and must not have any detrimental spells affecting them.
|
|
//
|
|
if(!RuleB(Character, RestRegenEnabled))
|
|
return;
|
|
|
|
RestRegenHP = RestRegenMana = RestRegenEndurance = 0;
|
|
|
|
if(IsEngaged() || !IsSitting())
|
|
return;
|
|
|
|
if(!rest_timer.Check(false))
|
|
return;
|
|
|
|
uint32 buff_count = GetMaxTotalSlots();
|
|
for (unsigned int j = 0; j < buff_count; j++) {
|
|
if(IsValidSpell(buffs[j].spellid)) {
|
|
if(IsDetrimentalSpell(buffs[j].spellid) && (buffs[j].ticsremaining > 0))
|
|
if(!IsRestAllowedSpell(buffs[j].spellid))
|
|
return;
|
|
}
|
|
}
|
|
|
|
RestRegenHP = 6 * (GetMaxHP() / zone->newzone_data.fast_regen_hp);
|
|
|
|
RestRegenMana = 6 * (GetMaxMana() / zone->newzone_data.fast_regen_mana);
|
|
|
|
RestRegenEndurance = 6 * (GetMaxEndurance() / zone->newzone_data.fast_regen_endurance);
|
|
}
|
|
|
|
bool Merc::HasSkill(EQ::skills::SkillType skill_id) const {
|
|
return ((GetSkill(skill_id) > 0) && CanHaveSkill(skill_id));
|
|
}
|
|
|
|
bool Merc::CanHaveSkill(EQ::skills::SkillType skill_id) const {
|
|
return skill_caps.GetSkillCap(GetClass(), skill_id, RuleI(Character, MaxLevel)).cap > 0;
|
|
//if you don't have it by max level, then odds are you never will?
|
|
}
|
|
|
|
uint16 Merc::MaxSkill(EQ::skills::SkillType skillid, uint16 class_, uint16 level) const {
|
|
return skill_caps.GetSkillCap(class_, skillid, level).cap;
|
|
}
|
|
|
|
void Merc::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) {
|
|
if(ns) {
|
|
Mob::FillSpawnStruct(ns, ForWho);
|
|
|
|
ns->spawn.afk = 0;
|
|
ns->spawn.lfg = 0;
|
|
ns->spawn.anon = 0;
|
|
ns->spawn.gm = 0;
|
|
ns->spawn.guildID = 0xFFFFFFFF; // 0xFFFFFFFF = NO GUILD, 0 = Unknown Guild
|
|
ns->spawn.is_npc = 1; // 0=no, 1=yes
|
|
ns->spawn.is_pet = 0;
|
|
ns->spawn.guildrank = 0;
|
|
ns->spawn.showhelm = 1;
|
|
ns->spawn.flymode = 0;
|
|
ns->spawn.NPC = 1; // 0=player,1=npc,2=pc corpse,3=npc corpse
|
|
ns->spawn.IsMercenary = 1;
|
|
ns->spawn.show_name = true;
|
|
|
|
UpdateActiveLight();
|
|
ns->spawn.light = m_Light.Type[EQ::lightsource::LightActive];
|
|
|
|
/*
|
|
// Wear Slots are not setup for Mercs yet
|
|
unsigned int i;
|
|
for (i = 0; i < _MaterialCount; i++)
|
|
{
|
|
if (equipment[i] == 0)
|
|
{
|
|
continue;
|
|
}
|
|
const ItemData* item = database.GetItem(equipment[i]);
|
|
if(item)
|
|
{
|
|
ns->spawn.equipment[i].material = item->Material;
|
|
ns->spawn.equipment[i].elitematerial = item->EliteMaterial;
|
|
ns->spawn.equipment[i].heroforgemodel = item->HerosForgeModel;
|
|
if (armor_tint[i])
|
|
{
|
|
ns->spawn.colors[i].color = armor_tint[i];
|
|
}
|
|
else
|
|
{
|
|
ns->spawn.colors[i].color = item->Color;
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
bool Merc::Process()
|
|
{
|
|
if(IsStunned() && stunned_timer.Check())
|
|
Mob::UnStun();
|
|
|
|
if (GetDepop())
|
|
{
|
|
SetMercCharacterID(0);
|
|
SetOwnerID(0);
|
|
return false;
|
|
}
|
|
|
|
if(!GetMercenaryOwner()) {
|
|
//p_depop = true; //this was causing a crash - removed merc from entity list, but not group
|
|
//return false; //merc can live after client dies, not sure how long
|
|
}
|
|
|
|
if(IsSuspended())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasGroup() && GetMercenaryOwner() && GetFollowID() == 0) {
|
|
SetFollowID(GetMercenaryOwner()->GetID());
|
|
}
|
|
|
|
SpellProcess();
|
|
|
|
if(tic_timer.Check())
|
|
{
|
|
//6 seconds, or whatever the rule is set to has passed, send this position to everyone to avoid ghosting
|
|
if (!IsEngaged())
|
|
{
|
|
SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0);
|
|
if (!rest_timer.Enabled()) {
|
|
rest_timer.Start(RuleI(Character, RestRegenTimeToActivate) * 1000);
|
|
}
|
|
}
|
|
|
|
BuffProcess();
|
|
|
|
CalcRestState();
|
|
|
|
if(GetHP() < GetMaxHP())
|
|
SetHP(GetHP() + CalcHPRegen() + RestRegenHP);
|
|
|
|
if(GetMana() < GetMaxMana())
|
|
SetMana(GetMana() + CalcManaRegen() + RestRegenMana);
|
|
|
|
if(GetEndurance() < GetMaxEndurance())
|
|
SetEndurance(GetEndurance() + CalcEnduranceRegen() + RestRegenEndurance);
|
|
}
|
|
|
|
if(confidence_timer.Check()) {
|
|
_check_confidence = true;
|
|
}
|
|
|
|
if (send_hp_update_timer.Check()) {
|
|
SendHPUpdate();
|
|
}
|
|
|
|
if (endupkeep_timer.Check() && GetHP() > 0){
|
|
DoEnduranceUpkeep();
|
|
}
|
|
|
|
if (IsStunned() || IsMezzed())
|
|
return true;
|
|
|
|
// Merc AI
|
|
AI_Process();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Merc::IsMercCasterCombatRange(Mob *target) {
|
|
bool result = false;
|
|
|
|
if(target) {
|
|
float range = MercAISpellRange;
|
|
|
|
range *= range;
|
|
|
|
// half the max so the merc doesn't always stop at max range to allow combat movement
|
|
range *= .5;
|
|
|
|
float targetDistance = DistanceSquaredNoZ(m_Position, target->GetPosition());
|
|
|
|
if(targetDistance > range)
|
|
result = false;
|
|
else
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void Merc::AI_Process() {
|
|
if(!IsAIControlled())
|
|
return;
|
|
|
|
if(IsCasting())
|
|
return;
|
|
|
|
// A merc wont start its AI if not grouped
|
|
if(!HasGroup()) {
|
|
return;
|
|
}
|
|
|
|
Mob* MercOwner = GetOwner();
|
|
|
|
if(GetAppearance() == eaDead)
|
|
{
|
|
if(!MercOwner)
|
|
{
|
|
Depop();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// The merc needs an owner
|
|
if(!MercOwner) {
|
|
//SetTarget(0);
|
|
//SetOwnerID(0);
|
|
// TODO: Need to wait and try casting rez if merc is a healer with a dead owner
|
|
return;
|
|
}
|
|
|
|
/*
|
|
try {
|
|
if(MercOwner->CastToClient()->IsDead()) {
|
|
SetTarget(0);
|
|
SetOwnerID(0);
|
|
return;
|
|
}
|
|
}
|
|
catch(...) {
|
|
SetTarget(0);
|
|
SetOwnerID(0);
|
|
return;
|
|
}
|
|
*/
|
|
|
|
if(check_target_timer.Check()) {
|
|
CheckHateList();
|
|
}
|
|
|
|
if(IsEngaged())
|
|
{
|
|
if(rest_timer.Enabled())
|
|
rest_timer.Disable();
|
|
|
|
if(IsRooted())
|
|
SetTarget(hate_list.GetClosestEntOnHateList(this));
|
|
else
|
|
FindTarget();
|
|
|
|
if(!GetTarget())
|
|
return;
|
|
|
|
if(HasPet())
|
|
GetPet()->SetTarget(GetTarget());
|
|
|
|
if(!IsSitting())
|
|
FaceTarget(GetTarget());
|
|
|
|
if(DivineAura())
|
|
return;
|
|
|
|
int hateCount = entity_list.GetHatedCount(this, nullptr, false);
|
|
if(GetHatedCount() < hateCount) {
|
|
SetHatedCount(hateCount);
|
|
|
|
if(!CheckConfidence()) {
|
|
if(!confidence_timer.Enabled()) {
|
|
confidence_timer.Start(10000);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Check specific conditions for merc to lose confidence and flee (or regain confidence once fleeing)
|
|
if(_check_confidence) {
|
|
//not already running
|
|
if(!_lost_confidence) {
|
|
//and fail confidence check
|
|
if(!CheckConfidence()) {
|
|
_lost_confidence = true;
|
|
|
|
//move to bottom of hate lists?
|
|
//Iterate though hatelist
|
|
// SetHate(other, hate, damage)
|
|
|
|
if(RuleB(Combat, EnableFearPathing)) {
|
|
CalculateNewFearpoint();
|
|
if(currently_fleeing) {
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
Stun(12000 - (6000 - tic_timer.GetRemainingTime()));
|
|
}
|
|
}
|
|
}
|
|
else { //are fleeing due to lost confidence
|
|
if(CheckConfidence()) { //passed test - regain confidence
|
|
_lost_confidence = false;
|
|
}
|
|
}
|
|
|
|
//they are in flee mode
|
|
if(_lost_confidence)
|
|
return;
|
|
}
|
|
|
|
// Let's check if we have a los with our target.
|
|
// If we don't, our hate_list is wiped.
|
|
// Else, it was causing the merc to aggro behind wall etc... causing massive trains.
|
|
if(GetTarget()->IsMezzed() || !IsAttackAllowed(GetTarget())) {
|
|
WipeHateList();
|
|
|
|
if(IsMoving()) {
|
|
SetHeading(0);
|
|
SetRunAnimSpeed(0);
|
|
|
|
if(moved) {
|
|
moved = false;
|
|
StopNavigation();
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
else if (!CheckLosFN(GetTarget())) {
|
|
auto Goal = GetTarget()->GetPosition();
|
|
RunTo(Goal.x, Goal.y, Goal.z);
|
|
|
|
return;
|
|
}
|
|
|
|
if (!(GetPlayerState() & static_cast<uint32>(PlayerState::Aggressive)))
|
|
SendAddPlayerState(PlayerState::Aggressive);
|
|
|
|
bool atCombatRange = false;
|
|
|
|
float meleeDistance = GetMaxMeleeRangeToTarget(GetTarget());
|
|
|
|
if(GetClass() == Class::ShadowKnight || GetClass() == Class::Paladin || GetClass() == Class::Warrior) {
|
|
meleeDistance = meleeDistance * .30;
|
|
}
|
|
else {
|
|
meleeDistance *= (float)zone->random.Real(.50, .85);
|
|
}
|
|
if(IsMercCaster() && GetLevel() > 12) {
|
|
if(IsMercCasterCombatRange(GetTarget()))
|
|
atCombatRange = true;
|
|
}
|
|
else if(DistanceSquared(m_Position, GetTarget()->GetPosition()) <= meleeDistance) {
|
|
atCombatRange = true;
|
|
}
|
|
|
|
if(atCombatRange)
|
|
{
|
|
if(IsMoving())
|
|
{
|
|
SetHeading(CalculateHeadingToTarget(GetTarget()->GetX(), GetTarget()->GetY()));
|
|
SetRunAnimSpeed(0);
|
|
|
|
if(moved) {
|
|
StopNavigation();
|
|
}
|
|
}
|
|
|
|
if(AI_movement_timer->Check()) {
|
|
if (!IsMoving()) {
|
|
if (GetClass() == Class::Rogue) {
|
|
if (HasTargetReflection() && !GetTarget()->IsFeared() && !GetTarget()->IsStunned()) {
|
|
// Hate redux actions
|
|
if (evade_timer.Check(false)) {
|
|
// Attempt to evade
|
|
int timer_duration = (HideReuseTime - GetSkillReuseTime(EQ::skills::SkillHide)) * 1000;
|
|
if (timer_duration < 0)
|
|
timer_duration = 0;
|
|
evade_timer.Start(timer_duration);
|
|
|
|
if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillHide))
|
|
RogueEvade(GetTarget());
|
|
|
|
return;
|
|
}
|
|
else if (GetTarget()->IsRooted()) {
|
|
// Move rogue back from rooted mob - out of combat range, if necessary
|
|
float melee_distance = GetMaxMeleeRangeToTarget(GetTarget());
|
|
float current_distance = DistanceSquared(static_cast<glm::vec3>(m_Position), static_cast<glm::vec3>(GetTarget()->GetPosition()));
|
|
|
|
if (current_distance <= melee_distance) {
|
|
float newX = 0;
|
|
float newY = 0;
|
|
float newZ = 0;
|
|
FaceTarget(GetTarget());
|
|
if (PlotPositionAroundTarget(this, newX, newY, newZ)) {
|
|
RunTo(newX, newY, newZ);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (!BehindMob(GetTarget(), GetX(), GetY())) {
|
|
// Move the rogue to behind the mob
|
|
float newX = 0;
|
|
float newY = 0;
|
|
float newZ = 0;
|
|
if (PlotPositionAroundTarget(GetTarget(), newX, newY, newZ)) {
|
|
RunTo(newX, newY, newZ);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (GetClass() != Class::Rogue && (DistanceSquaredNoZ(m_Position, GetTarget()->GetPosition()) < GetTarget()->GetSize())) {
|
|
// If we are not a rogue trying to backstab, let's try to adjust our melee range so we don't appear to be bunched up
|
|
float newX = 0;
|
|
float newY = 0;
|
|
float newZ = 0;
|
|
if (PlotPositionAroundTarget(GetTarget(), newX, newY, newZ, false) && GetArchetype() != Archetype::Caster) {
|
|
RunTo(newX, newY, newZ);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//if (IsMoving())
|
|
// SendPositionUpdate();
|
|
//else
|
|
// SendPosition();
|
|
}
|
|
|
|
if(!IsMercCaster() && GetTarget() && !IsStunned() && !IsMezzed() && (GetAppearance() != eaDead))
|
|
{
|
|
// we can't fight if we don't have a target, are stun/mezzed or dead..
|
|
// Stop attacking if the target is enraged
|
|
if(IsEngaged() && !BehindMob(GetTarget(), GetX(), GetY()) && GetTarget()->IsEnraged())
|
|
return;
|
|
//TODO: Implement Stances.
|
|
/*if(GetBotStance() == BotStancePassive)
|
|
return;*/
|
|
|
|
// First, special attack per class (kick, backstab etc..)
|
|
DoClassAttacks(GetTarget());
|
|
|
|
//try main hand first
|
|
if(attack_timer.Check())
|
|
{
|
|
Attack(GetTarget(), EQ::invslot::slotPrimary);
|
|
|
|
bool tripleSuccess = false;
|
|
|
|
if(GetOwner() && GetTarget() && CanThisClassDoubleAttack())
|
|
{
|
|
if(GetOwner()) {
|
|
Attack(GetTarget(), EQ::invslot::slotPrimary, true);
|
|
}
|
|
|
|
if(GetOwner() && GetTarget() && GetSpecialAbility(SpecialAbility::TripleAttack)) {
|
|
tripleSuccess = true;
|
|
Attack(GetTarget(), EQ::invslot::slotPrimary, true);
|
|
}
|
|
|
|
//quad attack, does this belong here??
|
|
if(GetOwner() && GetTarget() && GetSpecialAbility(SpecialAbility::QuadrupleAttack)) {
|
|
Attack(GetTarget(), EQ::invslot::slotPrimary, true);
|
|
}
|
|
}
|
|
|
|
//Live AA - Flurry, Rapid Strikes ect (Flurry does not require Triple Attack).
|
|
int16 flurrychance = aabonuses.FlurryChance + spellbonuses.FlurryChance + itembonuses.FlurryChance;
|
|
|
|
if (GetTarget() && flurrychance)
|
|
{
|
|
if(zone->random.Roll(flurrychance))
|
|
{
|
|
MessageString(Chat::NPCFlurry, YOU_FLURRY);
|
|
Attack(GetTarget(), EQ::invslot::slotPrimary, false);
|
|
Attack(GetTarget(), EQ::invslot::slotPrimary, false);
|
|
}
|
|
}
|
|
|
|
int16 ExtraAttackChanceBonus = spellbonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE] + itembonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE] + aabonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE];
|
|
|
|
if (GetTarget() && ExtraAttackChanceBonus) {
|
|
if(zone->random.Roll(ExtraAttackChanceBonus))
|
|
{
|
|
Attack(GetTarget(), EQ::invslot::slotPrimary, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Do mercs berserk? Find this out on live...
|
|
//if (GetClass() == Class::Warrior || GetClass() == Class::Berserker) {
|
|
// if(GetHP() > 0 && !berserk && GetHPRatio() < 30) {
|
|
// entity_list.MessageCloseString(this, false, 200, 0, BERSERK_START, GetName());
|
|
// berserk = true;
|
|
// }
|
|
// if (berserk && GetHPRatio() > 30) {
|
|
// entity_list.MessageCloseString(this, false, 200, 0, BERSERK_END, GetName());
|
|
// berserk = false;
|
|
// }
|
|
//}
|
|
|
|
//now off hand
|
|
if(GetTarget() && attack_dw_timer.Check() && CanThisClassDualWield())
|
|
{
|
|
int weapontype = 0; // No weapon type
|
|
bool bIsFist = true;
|
|
|
|
// why are we checking 'weapontype' when we know it's set to '0' above?
|
|
if (bIsFist || ((weapontype != EQ::item::ItemType2HSlash) && (weapontype != EQ::item::ItemType2HPiercing) && (weapontype != EQ::item::ItemType2HBlunt)))
|
|
{
|
|
float DualWieldProbability = 0.0f;
|
|
|
|
int16 Ambidexterity = aabonuses.Ambidexterity + spellbonuses.Ambidexterity + itembonuses.Ambidexterity;
|
|
DualWieldProbability = (GetSkill(EQ::skills::SkillDualWield) + GetLevel() + Ambidexterity) / 400.0f; // 78.0 max
|
|
int16 DWBonus = spellbonuses.DualWieldChance + itembonuses.DualWieldChance;
|
|
DualWieldProbability += DualWieldProbability*float(DWBonus)/ 100.0f;
|
|
|
|
// Max 78% of DW
|
|
if (zone->random.Roll(DualWieldProbability))
|
|
{
|
|
Attack(GetTarget(), EQ::invslot::slotSecondary); // Single attack with offhand
|
|
|
|
if(CanThisClassDoubleAttack()) {
|
|
if(GetTarget() && GetTarget()->GetHP() > -10)
|
|
Attack(GetTarget(), EQ::invslot::slotSecondary); // Single attack with offhand
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(GetTarget()->IsFeared() && !spellend_timer.Enabled()) {
|
|
// This is a mob that is fleeing either because it has been feared or is low on hitpoints
|
|
//TODO: Implement Stances.
|
|
//if(GetStance() != MercStancePassive)
|
|
AI_PursueCastCheck();
|
|
}
|
|
|
|
if (AI_movement_timer->Check())
|
|
{
|
|
if(!IsRooted()) {
|
|
LogAIDetail("Pursuing [{}] while engaged", GetTarget()->GetCleanName());
|
|
RunTo(GetTarget()->GetX(), GetTarget()->GetY(), GetTarget()->GetZ());
|
|
return;
|
|
}
|
|
|
|
//if(IsMoving())
|
|
// SendPositionUpdate();
|
|
//else
|
|
// SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0);
|
|
}
|
|
} // end not in combat range
|
|
|
|
if(!IsMoving() && !spellend_timer.Enabled())
|
|
{
|
|
//TODO: Implement Stances.
|
|
//if(GetStance() == MercStancePassive)
|
|
// return;
|
|
|
|
if(AI_EngagedCastCheck()) {
|
|
MercMeditate(false);
|
|
}
|
|
else if(GetArchetype() == Archetype::Caster)
|
|
MercMeditate(true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Not engaged in combat
|
|
SetTarget(0);
|
|
SetHatedCount(0);
|
|
confidence_timer.Disable();
|
|
_check_confidence = false;
|
|
|
|
if (GetPlayerState() & static_cast<uint32>(PlayerState::Aggressive))
|
|
SendRemovePlayerState(PlayerState::Aggressive);
|
|
|
|
if(!check_target_timer.Enabled())
|
|
check_target_timer.Start(2000, false);
|
|
|
|
if(!IsMoving() && AI_think_timer->Check() && !spellend_timer.Enabled())
|
|
{
|
|
//TODO: Implement passive stances.
|
|
//if(GetStance() != MercStancePassive) {
|
|
if(!AI_IdleCastCheck() && !IsCasting()) {
|
|
if(GetArchetype() == Archetype::Caster) {
|
|
MercMeditate(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(AI_movement_timer->Check()) {
|
|
if(GetFollowID()) {
|
|
Mob* follow = entity_list.GetMob(GetFollowID());
|
|
|
|
if (follow) {
|
|
float dist = DistanceSquared(m_Position, follow->GetPosition());
|
|
bool running = true;
|
|
|
|
if (dist < GetFollowDistance() + 1000)
|
|
running = false;
|
|
|
|
SetRunAnimSpeed(0);
|
|
|
|
if (dist > GetFollowDistance()) {
|
|
if (running) {
|
|
RunTo(follow->GetX(), follow->GetY(), follow->GetZ());
|
|
}
|
|
else {
|
|
WalkTo(follow->GetX(), follow->GetY(), follow->GetZ());
|
|
}
|
|
|
|
if (rest_timer.Enabled())
|
|
rest_timer.Disable();
|
|
}
|
|
else {
|
|
if (moved) {
|
|
moved = false;
|
|
StopNavigation();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Merc::AI_Start(int32 iMoveDelay) {
|
|
if (!pAIControlled)
|
|
return;
|
|
|
|
if (merc_spells.empty()) {
|
|
AIautocastspell_timer->SetTimer(1000);
|
|
AIautocastspell_timer->Disable();
|
|
} else {
|
|
AIautocastspell_timer->SetTimer(750);
|
|
AIautocastspell_timer->Start(RandomTimer(0, 2000), false);
|
|
}
|
|
|
|
if (NPCTypedata_ours) {
|
|
ProcessSpecialAbilities(NPCTypedata_ours->special_abilities);
|
|
}
|
|
|
|
SendTo(GetX(), GetY(), GetZ());
|
|
SaveGuardSpot(GetPosition());
|
|
}
|
|
|
|
void Merc::AI_Stop() {
|
|
NPC::AI_Stop();
|
|
Mob::AI_Stop();
|
|
}
|
|
|
|
bool Merc::AI_EngagedCastCheck() {
|
|
bool result = false;
|
|
bool failedToCast = false;
|
|
|
|
if (GetTarget() && AIautocastspell_timer->Check(false))
|
|
{
|
|
AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting.
|
|
|
|
LogAIDetail("Merc Engaged autocast check triggered");
|
|
|
|
int8 mercClass = GetClass();
|
|
|
|
switch(mercClass)
|
|
{
|
|
case TANK:
|
|
if (!AICastSpell(GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) {
|
|
if (!AICastSpell(GetChanceToCastBySpellType(SpellType_InCombatBuff), SpellType_InCombatBuff)) {
|
|
failedToCast = true;
|
|
}
|
|
}
|
|
break;
|
|
case HEALER:
|
|
if(!entity_list.Merc_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Heal), MercAISpellRange, SpellType_Heal)) {
|
|
if(!entity_list.Merc_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(SpellType_Buff), MercAISpellRange, SpellType_Buff)) {
|
|
failedToCast = true;
|
|
}
|
|
}
|
|
break;
|
|
case MELEEDPS:
|
|
if (!AICastSpell(GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) {
|
|
if (!AICastSpell(GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) {
|
|
if (!AICastSpell(GetChanceToCastBySpellType(SpellType_InCombatBuff), SpellType_InCombatBuff)) {
|
|
failedToCast = true;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case CASTERDPS:
|
|
if (!AICastSpell(GetChanceToCastBySpellType(SpellType_Escape), SpellType_Escape)) {
|
|
if (!AICastSpell(GetChanceToCastBySpellType(SpellType_Nuke), SpellType_Nuke)) {
|
|
failedToCast = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if(!AIautocastspell_timer->Enabled()) {
|
|
AIautocastspell_timer->Start(RandomTimer(100, 250), false);
|
|
}
|
|
|
|
if(!failedToCast)
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool Merc::AI_IdleCastCheck() {
|
|
bool result = false;
|
|
bool failedToCast = false;
|
|
|
|
if (AIautocastspell_timer->Check(false)) {
|
|
#if MercAI_DEBUG_Spells >= 25
|
|
LogAIDetail("Merc Non-Engaged autocast check triggered: [{}]", GetCleanName());
|
|
#endif
|
|
AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting.
|
|
|
|
//Ok, IdleCastCheck depends of class.
|
|
int8 mercClass = GetClass();
|
|
|
|
switch(mercClass)
|
|
{
|
|
case TANK:
|
|
failedToCast = true;
|
|
break;
|
|
case HEALER:
|
|
if(!entity_list.Merc_AICheckCloseBeneficialSpells(this, 100, MercAISpellRange, SpellType_Cure)) {
|
|
if(!entity_list.Merc_AICheckCloseBeneficialSpells(this, 100, MercAISpellRange, SpellType_Heal)) {
|
|
if(!entity_list.Merc_AICheckCloseBeneficialSpells(this, 100, MercAISpellRange, SpellType_Resurrect)) {
|
|
if(!entity_list.Merc_AICheckCloseBeneficialSpells(this, 100, MercAISpellRange, SpellType_Buff)) {
|
|
failedToCast = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
result = true;
|
|
break;
|
|
case MELEEDPS:
|
|
if(!entity_list.Merc_AICheckCloseBeneficialSpells(this, 100, MercAISpellRange, SpellType_Buff)) {
|
|
failedToCast = true;
|
|
}
|
|
break;
|
|
case CASTERDPS:
|
|
failedToCast = true;
|
|
break;
|
|
}
|
|
|
|
if(!AIautocastspell_timer->Enabled())
|
|
AIautocastspell_timer->Start(RandomTimer(500, 1000), false);
|
|
|
|
if(!failedToCast)
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool EntityList::Merc_AICheckCloseBeneficialSpells(Merc* caster, uint8 iChance, float iRange, uint32 iSpellTypes) {
|
|
|
|
if ((iSpellTypes & SPELL_TYPES_DETRIMENTAL) != 0) {
|
|
//according to live, you can buff and heal through walls...
|
|
//now with PCs, this only applies if you can TARGET the target, but
|
|
// according to Rogean, Live NPCs will just cast through walls/floors, no problem..
|
|
//
|
|
// This check was put in to address an idle-mob CPU issue
|
|
LogError("Error: detrimental spells requested from AICheckCloseBeneficialSpells!!");
|
|
return(false);
|
|
}
|
|
|
|
if(!caster)
|
|
return false;
|
|
|
|
if(!caster->AI_HasSpells())
|
|
return false;
|
|
|
|
if (iChance < 100) {
|
|
int8 tmp = zone->random.Int(1, 100);
|
|
if (tmp > iChance)
|
|
return false;
|
|
}
|
|
|
|
int8 mercCasterClass = caster->GetClass();
|
|
|
|
if(caster->HasGroup()) {
|
|
if( mercCasterClass == HEALER) {
|
|
if( iSpellTypes == SpellType_Heal ) {
|
|
if(caster->AICastSpell(100, SpellType_Heal))
|
|
return true;
|
|
}
|
|
|
|
if( iSpellTypes == SpellType_Cure ) {
|
|
if(caster->AICastSpell(100, SpellType_Cure))
|
|
return true;
|
|
}
|
|
|
|
if( iSpellTypes == SpellType_Resurrect ) {
|
|
if(caster->AICastSpell(100, SpellType_Resurrect))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//Ok for the buffs..
|
|
if( iSpellTypes == SpellType_Buff) {
|
|
if(caster->AICastSpell(100, SpellType_Buff))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Merc::AIDoSpellCast(uint16 spellid, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore) {
|
|
bool result = false;
|
|
MercSpell mercSpell = GetMercSpellBySpellID(this, spellid);
|
|
|
|
// manacost has special values, -1 is no mana cost, -2 is instant cast (no mana)
|
|
int32 manaCost = mana_cost;
|
|
|
|
if (manaCost == -1)
|
|
manaCost = spells[spellid].mana;
|
|
else if (manaCost == -2)
|
|
manaCost = 0;
|
|
|
|
int32 extraMana = 0;
|
|
int32 hasMana = GetMana();
|
|
|
|
float dist2 = 0;
|
|
|
|
if (mercSpell.type & SpellType_Escape) {
|
|
dist2 = 0;
|
|
} else
|
|
dist2 = DistanceSquared(m_Position, tar->GetPosition());
|
|
|
|
if (((((spells[spellid].target_type==ST_GroupTeleport && mercSpell.type==SpellType_Heal)
|
|
|| spells[spellid].target_type==ST_AECaster
|
|
|| spells[spellid].target_type==ST_Group
|
|
|| spells[spellid].target_type==ST_AEBard)
|
|
&& dist2 <= spells[spellid].aoe_range*spells[spellid].aoe_range)
|
|
|| dist2 <= GetActSpellRange(spellid, spells[spellid].range)*GetActSpellRange(spellid, spells[spellid].range)) && (mana_cost <= GetMana() || GetMana() == GetMaxMana()))
|
|
{
|
|
SetRunAnimSpeed(0);
|
|
SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0);
|
|
SetMoving(false);
|
|
|
|
result = CastSpell(spellid, tar->GetID(), EQ::spells::CastingSlot::Gem2, -1, mana_cost, oDontDoAgainBefore, -1, -1, 0, 0);
|
|
|
|
if(IsCasting() && IsSitting())
|
|
Stand();
|
|
}
|
|
|
|
// if the spell wasn't casted, then take back any extra mana that was given to the bot to cast that spell
|
|
if(!result) {
|
|
SetMana(hasMana);
|
|
extraMana = false;
|
|
}
|
|
else { //handle spell recast and recast timers
|
|
SetSpellTimeCanCast(mercSpell.spellid, spells[spellid].recast_time);
|
|
|
|
if(spells[spellid].timer_id > 0) {
|
|
SetSpellRecastTimer(spells[spellid].timer_id, spellid, spells[spellid].recast_time);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool Merc::AICastSpell(int8 iChance, uint32 iSpellTypes) {
|
|
|
|
if(!AI_HasSpells())
|
|
return false;
|
|
|
|
if (iChance < 100) {
|
|
if (zone->random.Int(0, 100) > iChance){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int8 mercClass = GetClass();
|
|
uint8 mercLevel = GetLevel();
|
|
|
|
bool checked_los = false; //we do not check LOS until we are absolutely sure we need to, and we only do it once.
|
|
bool castedSpell = false;
|
|
bool isDiscipline = false;
|
|
|
|
if(HasGroup()) {
|
|
Group *g = GetGroup();
|
|
|
|
if(g) {
|
|
MercSpell selectedMercSpell;
|
|
selectedMercSpell.spellid = 0;
|
|
selectedMercSpell.stance = 0;
|
|
selectedMercSpell.type = 0;
|
|
selectedMercSpell.slot = 0;
|
|
selectedMercSpell.proc_chance = 0;
|
|
selectedMercSpell.time_cancast = 0;
|
|
|
|
switch(mercClass)
|
|
{
|
|
case TANK:
|
|
case MELEEDPS:
|
|
isDiscipline = true;
|
|
break;
|
|
default:
|
|
isDiscipline = false;
|
|
break;
|
|
}
|
|
|
|
switch (iSpellTypes) {
|
|
case SpellType_Heal: {
|
|
Mob* tar = nullptr;
|
|
int8 numToHeal = g->GetNumberNeedingHealedInGroup(IsEngaged() ? 75 : 95, true);
|
|
int8 checkHPR = IsEngaged() ? 95 : 99;
|
|
int8 checkPetHPR = IsEngaged() ? 95 : 99;
|
|
|
|
//todo: check stance to determine healing spell selection
|
|
|
|
for(int i = 0; i < MAX_GROUP_MEMBERS; i++) {
|
|
if(g->members[i] && !g->members[i]->qglobal) {
|
|
int8 hpr = (int8)g->members[i]->GetHPRatio();
|
|
|
|
if(g->members[i]->HasPet() && g->members[i]->GetPet()->GetHPRatio() < checkHPR) {
|
|
if(!tar || ((g->members[i]->GetPet()->GetHPRatio() + 25) < tar->GetHPRatio())) {
|
|
tar = g->members[i]->GetPet();
|
|
checkPetHPR = g->members[i]->GetPet()->GetHPRatio() + 25;
|
|
}
|
|
}
|
|
|
|
if(hpr > checkHPR) {
|
|
continue;
|
|
}
|
|
|
|
if(IsEngaged() && (g->members[i]->GetClass() == Class::Necromancer && hpr >= 50)
|
|
|| (g->members[i]->GetClass() == Class::Shaman && hpr >= 80)) {
|
|
//allow necros to lifetap & shaman to canni without wasting mana
|
|
continue;
|
|
}
|
|
|
|
if(hpr < checkHPR && g->members[i] == GetMercenaryOwner()) {
|
|
if(!tar || (hpr < tar->GetHPRatio() || (tar->IsPet() && hpr < checkPetHPR)))
|
|
tar = g->members[i]; //check owner first
|
|
}
|
|
else if(hpr < checkHPR && g->HasRole(g->members[i], RoleTank)){
|
|
if(!tar || (hpr < tar->GetHPRatio() || (tar->IsPet() && hpr < checkPetHPR)))
|
|
tar = g->members[i];
|
|
}
|
|
else if( hpr < checkHPR && (!tar || (hpr < tar->GetHPRatio() || (tar->IsPet() && hpr < checkPetHPR)))) {
|
|
tar = g->members[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if(numToHeal > 2) {
|
|
selectedMercSpell = GetBestMercSpellForGroupHeal(this);
|
|
}
|
|
|
|
if(tar && selectedMercSpell.spellid == 0) {
|
|
if(tar->GetHPRatio() < 15) {
|
|
//check for very fast heals first (casting time < 1 s)
|
|
selectedMercSpell = GetBestMercSpellForVeryFastHeal(this);
|
|
|
|
//check for fast heals next (casting time < 2 s)
|
|
if(selectedMercSpell.spellid == 0) {
|
|
selectedMercSpell = GetBestMercSpellForFastHeal(this);
|
|
}
|
|
|
|
//get regular heal
|
|
if(selectedMercSpell.spellid == 0) {
|
|
selectedMercSpell = GetBestMercSpellForRegularSingleTargetHeal(this);
|
|
}
|
|
}
|
|
else if (tar->GetHPRatio() < 35) {
|
|
//check for fast heals next (casting time < 2 s)
|
|
selectedMercSpell = GetBestMercSpellForFastHeal(this);
|
|
|
|
//get regular heal
|
|
if(selectedMercSpell.spellid == 0) {
|
|
selectedMercSpell = GetBestMercSpellForRegularSingleTargetHeal(this);
|
|
}
|
|
}
|
|
else if (tar->GetHPRatio() < 80) {
|
|
selectedMercSpell = GetBestMercSpellForPercentageHeal(this);
|
|
|
|
//get regular heal
|
|
if(selectedMercSpell.spellid == 0) {
|
|
selectedMercSpell = GetBestMercSpellForRegularSingleTargetHeal(this);
|
|
}
|
|
}
|
|
else {
|
|
//check for heal over time. if not present, try it first
|
|
if (!tar->FindType(SE_HealOverTime)) {
|
|
selectedMercSpell = GetBestMercSpellForHealOverTime(this);
|
|
|
|
//get regular heal
|
|
if (selectedMercSpell.spellid == 0) {
|
|
selectedMercSpell = GetBestMercSpellForRegularSingleTargetHeal(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(selectedMercSpell.spellid > 0) {
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, tar, -1);
|
|
}
|
|
|
|
if(castedSpell) {
|
|
if(tar && tar != this) { // [tar] was implicitly valid at this point..this change is to catch any bad logic
|
|
//we don't need spam of bots healing themselves
|
|
MercGroupSay(this, "Casting %s on %s.", spells[selectedMercSpell.spellid].name, tar->GetCleanName());
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case SpellType_Root: {
|
|
break;
|
|
}
|
|
case SpellType_Buff: {
|
|
|
|
if(GetClass() == HEALER && GetManaRatio() < 50) {
|
|
return false; //mercs buff when Mana > 50%
|
|
}
|
|
|
|
std::list<MercSpell> buffSpellList = GetMercSpellsBySpellType(this, SpellType_Buff);
|
|
|
|
for (auto itr = buffSpellList.begin();
|
|
itr != buffSpellList.end(); ++itr) {
|
|
MercSpell selectedMercSpell = *itr;
|
|
|
|
if(!((spells[selectedMercSpell.spellid].target_type == ST_Target || spells[selectedMercSpell.spellid].target_type == ST_Pet ||
|
|
spells[selectedMercSpell.spellid].target_type == ST_Group || spells[selectedMercSpell.spellid].target_type == ST_GroupTeleport ||
|
|
spells[selectedMercSpell.spellid].target_type == ST_Self))) {
|
|
continue;
|
|
}
|
|
|
|
if(spells[selectedMercSpell.spellid].target_type == ST_Self) {
|
|
if( !IsImmuneToSpell(selectedMercSpell.spellid, this)
|
|
&& (CanBuffStack(selectedMercSpell.spellid, mercLevel, true) >= 0)) {
|
|
|
|
if( GetArchetype() == Archetype::Melee && IsEffectInSpell(selectedMercSpell.spellid, SE_IncreaseSpellHaste)) {
|
|
continue;
|
|
}
|
|
|
|
uint32 TempDontBuffMeBeforeTime = DontBuffMeBefore();
|
|
|
|
if(selectedMercSpell.spellid > 0) {
|
|
if(isDiscipline) {
|
|
castedSpell = UseDiscipline(selectedMercSpell.spellid, GetID());
|
|
}
|
|
else {
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, this, -1, &TempDontBuffMeBeforeTime);
|
|
|
|
if(TempDontBuffMeBeforeTime != DontBuffMeBefore())
|
|
SetDontBuffMeBefore(TempDontBuffMeBeforeTime);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for( int i = 0; i < MAX_GROUP_MEMBERS; i++) {
|
|
if(g->members[i]) {
|
|
Mob* tar = g->members[i];
|
|
|
|
if( !tar->IsImmuneToSpell(selectedMercSpell.spellid, this)
|
|
&& (tar->CanBuffStack(selectedMercSpell.spellid, mercLevel, true) >= 0)) {
|
|
|
|
if( tar->GetArchetype() == Archetype::Melee && IsEffectInSpell(selectedMercSpell.spellid, SE_IncreaseSpellHaste)) {
|
|
continue;
|
|
}
|
|
|
|
uint32 TempDontBuffMeBeforeTime = tar->DontBuffMeBefore();
|
|
|
|
if(selectedMercSpell.spellid > 0) {
|
|
if(isDiscipline) {
|
|
castedSpell = UseDiscipline(selectedMercSpell.spellid, tar->GetID());
|
|
}
|
|
else {
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, tar, -1, &TempDontBuffMeBeforeTime);
|
|
|
|
if(TempDontBuffMeBeforeTime != tar->DontBuffMeBefore())
|
|
tar->SetDontBuffMeBefore(TempDontBuffMeBeforeTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!castedSpell && tar->GetPet()) {
|
|
|
|
//don't cast group spells on pets
|
|
if(IsGroupSpell(selectedMercSpell.spellid)
|
|
|| spells[selectedMercSpell.spellid].target_type == ST_Group
|
|
|| spells[selectedMercSpell.spellid].target_type == ST_GroupTeleport ) {
|
|
continue;
|
|
}
|
|
|
|
if(!tar->GetPet()->IsImmuneToSpell(selectedMercSpell.spellid, this)
|
|
&& (tar->GetPet()->CanBuffStack(selectedMercSpell.spellid, mercLevel, true) >= 0)) {
|
|
|
|
uint32 TempDontBuffMeBeforeTime = tar->DontBuffMeBefore();
|
|
|
|
if(selectedMercSpell.spellid > 0) {
|
|
if(isDiscipline) {
|
|
castedSpell = UseDiscipline(selectedMercSpell.spellid, tar->GetPet()->GetID());
|
|
}
|
|
else {
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, tar->GetPet(), -1, &TempDontBuffMeBeforeTime);
|
|
|
|
if(TempDontBuffMeBeforeTime != tar->GetPet()->DontBuffMeBefore())
|
|
tar->GetPet()->SetDontBuffMeBefore(TempDontBuffMeBeforeTime);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_Nuke: {
|
|
switch(mercClass)
|
|
{
|
|
case TANK:
|
|
//check for taunt
|
|
if(CheckAETaunt()) {
|
|
//get AE taunt
|
|
selectedMercSpell = GetBestMercSpellForAETaunt(this);
|
|
Log(Logs::General, Logs::Mercenaries, "%s AE Taunting.", GetName());
|
|
}
|
|
|
|
if(selectedMercSpell.spellid == 0 && CheckTaunt()) {
|
|
//get taunt
|
|
selectedMercSpell = GetBestMercSpellForTaunt(this);
|
|
}
|
|
|
|
//get hate disc
|
|
if(selectedMercSpell.spellid == 0) {
|
|
selectedMercSpell = GetBestMercSpellForHate(this);
|
|
}
|
|
|
|
break;
|
|
case HEALER:
|
|
break;
|
|
case MELEEDPS:
|
|
break;
|
|
case CASTERDPS:
|
|
Mob* tar = GetTarget();
|
|
|
|
selectedMercSpell = GetBestMercSpellForAENuke(this, tar);
|
|
|
|
if(selectedMercSpell.spellid == 0 && !tar->GetSpecialAbility(SpecialAbility::StunImmunity) && !tar->IsStunned()) {
|
|
uint8 stunChance = 15;
|
|
if(zone->random.Roll(stunChance)) {
|
|
selectedMercSpell = GetBestMercSpellForStun(this);
|
|
}
|
|
}
|
|
|
|
if(selectedMercSpell.spellid == 0) {
|
|
uint8 lureChance = 25;
|
|
if(zone->random.Roll(lureChance)) {
|
|
selectedMercSpell = GetBestMercSpellForNukeByTargetResists(this, tar);
|
|
}
|
|
}
|
|
|
|
if(selectedMercSpell.spellid == 0) {
|
|
selectedMercSpell = GetBestMercSpellForNuke(this);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if(selectedMercSpell.spellid > 0) {
|
|
if(isDiscipline) {
|
|
castedSpell = UseDiscipline(selectedMercSpell.spellid, GetTarget()->GetID());
|
|
}
|
|
else {
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, GetTarget(), -1);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case SpellType_InCombatBuff: {
|
|
std::list<MercSpell> buffSpellList = GetMercSpellsBySpellType(this, SpellType_InCombatBuff);
|
|
Mob* tar = this;
|
|
|
|
for (auto itr = buffSpellList.begin();
|
|
itr != buffSpellList.end(); ++itr) {
|
|
MercSpell selectedMercSpell = *itr;
|
|
|
|
if(!(spells[selectedMercSpell.spellid].target_type == ST_Self)) {
|
|
continue;
|
|
}
|
|
|
|
if (spells[selectedMercSpell.spellid].skill == EQ::skills::SkillBackstab && spells[selectedMercSpell.spellid].target_type == ST_Self) {
|
|
if(!hidden) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if( !tar->IsImmuneToSpell(selectedMercSpell.spellid, this)
|
|
&& (tar->CanBuffStack(selectedMercSpell.spellid, mercLevel, true) >= 0)) {
|
|
|
|
uint32 TempDontBuffMeBeforeTime = tar->DontBuffMeBefore();
|
|
|
|
if(selectedMercSpell.spellid > 0) {
|
|
if(isDiscipline) {
|
|
castedSpell = UseDiscipline(selectedMercSpell.spellid, GetID());
|
|
}
|
|
else {
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, this, -1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_Cure: {
|
|
Mob* tar = nullptr;
|
|
for(int i = 0; i < MAX_GROUP_MEMBERS; i++) {
|
|
if(g->members[i] && !g->members[i]->qglobal) {
|
|
if(GetNeedsCured(g->members[i]) && (g->members[i]->DontCureMeBefore() < Timer::GetCurrentTime())) {
|
|
tar = g->members[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if(tar && !(g->GetNumberNeedingHealedInGroup(IsEngaged() ? 25 : 40, false) > 0) && !(g->GetNumberNeedingHealedInGroup(IsEngaged() ? 40 : 60, false) > 2))
|
|
{
|
|
selectedMercSpell = GetBestMercSpellForCure(this, tar);
|
|
|
|
if(selectedMercSpell.spellid == 0)
|
|
break;
|
|
|
|
uint32 TempDontCureMeBeforeTime = tar->DontCureMeBefore();
|
|
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, tar, spells[selectedMercSpell.spellid].mana, &TempDontCureMeBeforeTime);
|
|
|
|
if(castedSpell) {
|
|
if(IsGroupSpell(selectedMercSpell.spellid)){
|
|
|
|
if(HasGroup()) {
|
|
Group *g = GetGroup();
|
|
|
|
if(g) {
|
|
for( int i = 0; i<MAX_GROUP_MEMBERS; i++) {
|
|
if(g->members[i] && !g->members[i]->qglobal) {
|
|
if(TempDontCureMeBeforeTime != tar->DontCureMeBefore())
|
|
g->members[i]->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if(TempDontCureMeBeforeTime != tar->DontCureMeBefore())
|
|
tar->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_Resurrect: {
|
|
Corpse *corpse = GetGroupMemberCorpse();
|
|
|
|
if(corpse) {
|
|
selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_Resurrect);
|
|
|
|
if(selectedMercSpell.spellid == 0)
|
|
break;
|
|
|
|
uint32 TempDontRootMeBeforeTime = corpse->DontRootMeBefore();
|
|
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, corpse, spells[selectedMercSpell.spellid].mana, &TempDontRootMeBeforeTime);
|
|
|
|
//CastSpell(selectedMercSpell.spellid, corpse->GetID(), 1, -1, -1, &TempDontRootMeBeforeTime);
|
|
corpse->SetDontRootMeBefore(TempDontRootMeBeforeTime);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case SpellType_Escape: {
|
|
Mob* tar = GetTarget();
|
|
uint8 hpr = (uint8)GetHPRatio();
|
|
bool mayGetAggro = false;
|
|
|
|
if(tar && (mercClass == CASTERDPS) || (mercClass == MELEEDPS)) {
|
|
mayGetAggro = HasOrMayGetAggro(); //classes have hate reducing spells
|
|
|
|
if (mayGetAggro) {
|
|
selectedMercSpell = GetFirstMercSpellBySpellType(this, SpellType_Escape);
|
|
|
|
if(selectedMercSpell.spellid == 0)
|
|
break;
|
|
|
|
if(isDiscipline) {
|
|
castedSpell = UseDiscipline(selectedMercSpell.spellid, tar->GetID());
|
|
}
|
|
else {
|
|
castedSpell = AIDoSpellCast(selectedMercSpell.spellid, tar, -1);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return castedSpell;
|
|
}
|
|
|
|
void Merc::CheckHateList() {
|
|
if(check_target_timer.Enabled())
|
|
check_target_timer.Disable();
|
|
|
|
if(!IsEngaged()) {
|
|
if(GetFollowID()) {
|
|
Group* g = GetGroup();
|
|
if(g) {
|
|
Mob* MercOwner = GetOwner();
|
|
if(MercOwner && MercOwner->GetTarget() && MercOwner->GetTarget()->IsNPC() && (MercOwner->GetTarget()->GetHateAmount(MercOwner) || MercOwner->CastToClient()->AutoAttackEnabled()) && IsAttackAllowed(MercOwner->GetTarget())) {
|
|
float range = g->HasRole(MercOwner, RolePuller) ? RuleI(Mercs, AggroRadiusPuller) : RuleI(Mercs, AggroRadius);
|
|
range = range * range;
|
|
if(DistanceSquaredNoZ(m_Position, MercOwner->GetTarget()->GetPosition()) < range) {
|
|
AddToHateList(MercOwner->GetTarget(), 1);
|
|
}
|
|
}
|
|
else {
|
|
std::list<NPC*> npc_list;
|
|
entity_list.GetNPCList(npc_list);
|
|
|
|
for (auto itr = npc_list.begin(); itr != npc_list.end(); ++itr) {
|
|
NPC* npc = *itr;
|
|
float dist = DistanceSquaredNoZ(m_Position, npc->GetPosition());
|
|
int radius = RuleI(Mercs, AggroRadius);
|
|
radius *= radius;
|
|
if(dist <= radius) {
|
|
|
|
for(int counter = 0; counter < g->GroupCount(); counter++) {
|
|
Mob* groupMember = g->members[counter];
|
|
if(groupMember) {
|
|
if(npc->IsOnHatelist(groupMember)) {
|
|
if(!hate_list.IsEntOnHateList(npc)) {
|
|
float range = g->HasRole(groupMember, RolePuller) ? RuleI(Mercs, AggroRadiusPuller) : RuleI(Mercs, AggroRadius);
|
|
range *= range;
|
|
if(DistanceSquaredNoZ(m_Position, npc->GetPosition()) < range) {
|
|
hate_list.AddEntToHateList(npc, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Merc::HasOrMayGetAggro() {
|
|
bool mayGetAggro = false;
|
|
|
|
if(GetTarget() && GetTarget()->GetHateTop()) {
|
|
Mob *topHate = GetTarget()->GetHateTop();
|
|
|
|
if(topHate == this)
|
|
mayGetAggro = true; //I currently have aggro
|
|
else {
|
|
uint32 myHateAmt = GetTarget()->GetHateAmount(this);
|
|
uint32 topHateAmt = GetTarget()->GetHateAmount(topHate);
|
|
|
|
if(myHateAmt > 0 && topHateAmt > 0 && (uint8)((myHateAmt/topHateAmt)*100) > 90) //I have 90% as much hate as top, next action may give me aggro
|
|
mayGetAggro = true;
|
|
}
|
|
}
|
|
|
|
return mayGetAggro;
|
|
}
|
|
|
|
bool Merc::CheckAENuke(Merc* caster, Mob* tar, uint16 spell_id, uint8 &numTargets) {
|
|
std::list<NPC*> npc_list;
|
|
entity_list.GetNPCList(npc_list);
|
|
|
|
for (auto itr = npc_list.begin(); itr != npc_list.end(); ++itr) {
|
|
NPC* npc = *itr;
|
|
|
|
if(DistanceSquaredNoZ(npc->GetPosition(), tar->GetPosition()) <= spells[spell_id].aoe_range * spells[spell_id].aoe_range) {
|
|
if(!npc->IsMezzed()) {
|
|
numTargets++;
|
|
}
|
|
else {
|
|
numTargets = 0;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(numTargets > 1)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
int64 Merc::GetFocusEffect(focusType type, uint16 spell_id, bool from_buff_tic) {
|
|
|
|
int32 realTotal = 0;
|
|
int32 realTotal2 = 0;
|
|
int32 realTotal3 = 0;
|
|
bool rand_effectiveness = false;
|
|
|
|
//Improved Healing, Damage & Mana Reduction are handled differently in that some are random percentages
|
|
//In these cases we need to find the most powerful effect, so that each piece of gear wont get its own chance
|
|
if((type == focusManaCost || type == focusImprovedHeal || type == focusImprovedDamage)
|
|
&& RuleB(Spells, LiveLikeFocusEffects))
|
|
{
|
|
rand_effectiveness = true;
|
|
}
|
|
|
|
//Check if item focus effect exists for the client.
|
|
if (itembonuses.FocusEffects[type]){
|
|
|
|
const EQ::ItemData* TempItem = nullptr;
|
|
const EQ::ItemData* UsedItem = nullptr;
|
|
int32 UsedFocusID = 0;
|
|
int32 Total = 0;
|
|
int32 focus_max = 0;
|
|
int32 focus_max_real = 0;
|
|
|
|
//item focus
|
|
for (int x = EQ::invslot::EQUIPMENT_BEGIN; x <= EQ::invslot::EQUIPMENT_END; ++x)
|
|
{
|
|
TempItem = nullptr;
|
|
if (equipment[x] == 0)
|
|
continue;
|
|
TempItem = database.GetItem(equipment[x]);
|
|
if (TempItem && IsValidSpell(TempItem->Focus.Effect)) {
|
|
if(rand_effectiveness) {
|
|
focus_max = CalcFocusEffect(type, TempItem->Focus.Effect, spell_id, true);
|
|
if (focus_max > 0 && focus_max_real >= 0 && focus_max > focus_max_real) {
|
|
focus_max_real = focus_max;
|
|
UsedItem = TempItem;
|
|
UsedFocusID = TempItem->Focus.Effect;
|
|
} else if (focus_max < 0 && focus_max < focus_max_real) {
|
|
focus_max_real = focus_max;
|
|
UsedItem = TempItem;
|
|
UsedFocusID = TempItem->Focus.Effect;
|
|
}
|
|
}
|
|
else {
|
|
Total = CalcFocusEffect(type, TempItem->Focus.Effect, spell_id);
|
|
if (Total > 0 && realTotal >= 0 && Total > realTotal) {
|
|
realTotal = Total;
|
|
UsedItem = TempItem;
|
|
UsedFocusID = TempItem->Focus.Effect;
|
|
} else if (Total < 0 && Total < realTotal) {
|
|
realTotal = Total;
|
|
UsedItem = TempItem;
|
|
UsedFocusID = TempItem->Focus.Effect;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(UsedItem && rand_effectiveness && focus_max_real != 0)
|
|
realTotal = CalcFocusEffect(type, UsedFocusID, spell_id);
|
|
|
|
if (realTotal != 0 && UsedItem)
|
|
MessageString(Chat::FocusEffect, BEGINS_TO_GLOW, UsedItem->Name);
|
|
}
|
|
|
|
//Check if spell focus effect exists for the client.
|
|
if (spellbonuses.FocusEffects[type]){
|
|
|
|
//Spell Focus
|
|
int32 Total2 = 0;
|
|
int32 focus_max2 = 0;
|
|
int32 focus_max_real2 = 0;
|
|
|
|
int buff_tracker = -1;
|
|
int buff_slot = 0;
|
|
int32 focusspellid = 0;
|
|
int32 focusspell_tracker = 0;
|
|
uint32 buff_max = GetMaxTotalSlots();
|
|
for (buff_slot = 0; buff_slot < buff_max; buff_slot++) {
|
|
focusspellid = buffs[buff_slot].spellid;
|
|
if (focusspellid == 0 || focusspellid >= SPDAT_RECORDS)
|
|
continue;
|
|
|
|
if(rand_effectiveness) {
|
|
focus_max2 = CalcFocusEffect(type, focusspellid, spell_id, true);
|
|
if (focus_max2 > 0 && focus_max_real2 >= 0 && focus_max2 > focus_max_real2) {
|
|
focus_max_real2 = focus_max2;
|
|
buff_tracker = buff_slot;
|
|
focusspell_tracker = focusspellid;
|
|
} else if (focus_max2 < 0 && focus_max2 < focus_max_real2) {
|
|
focus_max_real2 = focus_max2;
|
|
buff_tracker = buff_slot;
|
|
focusspell_tracker = focusspellid;
|
|
}
|
|
}
|
|
else {
|
|
Total2 = CalcFocusEffect(type, focusspellid, spell_id);
|
|
if (Total2 > 0 && realTotal2 >= 0 && Total2 > realTotal2) {
|
|
realTotal2 = Total2;
|
|
buff_tracker = buff_slot;
|
|
focusspell_tracker = focusspellid;
|
|
} else if (Total2 < 0 && Total2 < realTotal2) {
|
|
realTotal2 = Total2;
|
|
buff_tracker = buff_slot;
|
|
focusspell_tracker = focusspellid;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(focusspell_tracker && rand_effectiveness && focus_max_real2 != 0)
|
|
realTotal2 = CalcFocusEffect(type, focusspell_tracker, spell_id);
|
|
|
|
if (!from_buff_tic && buff_tracker >= 0 && buffs[buff_tracker].hit_number > 0) {
|
|
CheckNumHitsRemaining(NumHit::MatchingSpells, buff_tracker);
|
|
}
|
|
}
|
|
|
|
|
|
// AA Focus
|
|
/*if (aabonuses.FocusEffects[type]){
|
|
|
|
int16 Total3 = 0;
|
|
uint32 slots = 0;
|
|
uint32 aa_AA = 0;
|
|
uint32 aa_value = 0;
|
|
|
|
for (int i = 0; i < MAX_PP_AA_ARRAY; i++)
|
|
{
|
|
aa_AA = aa[i]->AA;
|
|
aa_value = aa[i]->value;
|
|
if (aa_AA < 1 || aa_value < 1)
|
|
continue;
|
|
|
|
Total3 = CalcAAFocus(type, aa_AA, spell_id);
|
|
if (Total3 > 0 && realTotal3 >= 0 && Total3 > realTotal3) {
|
|
realTotal3 = Total3;
|
|
}
|
|
else if (Total3 < 0 && Total3 < realTotal3) {
|
|
realTotal3 = Total3;
|
|
}
|
|
}
|
|
}*/
|
|
|
|
if(type == focusReagentCost && IsSummonPetSpell(spell_id) && GetAA(aaElementalPact))
|
|
return 100;
|
|
|
|
if(type == focusReagentCost && (IsEffectInSpell(spell_id, SE_SummonItem) || IsSacrificeSpell(spell_id)))
|
|
return 0;
|
|
//Summon Spells that require reagents are typically imbue type spells, enchant metal, sacrifice and shouldn't be affected
|
|
//by reagent conservation for obvious reasons.
|
|
|
|
return realTotal + realTotal2 + realTotal3;
|
|
}
|
|
|
|
int8 Merc::GetChanceToCastBySpellType(uint32 spellType) {
|
|
int mercStance = (int)GetStance();
|
|
int8 mercClass = GetClass();
|
|
int8 chance = 0;
|
|
|
|
switch (spellType) {
|
|
case SpellType_Nuke: {
|
|
switch(mercClass)
|
|
{
|
|
case TANK: {
|
|
chance = 100;
|
|
break;
|
|
}
|
|
case HEALER:{
|
|
break;
|
|
}
|
|
case MELEEDPS:{
|
|
chance = 100;
|
|
break;
|
|
}
|
|
case CASTERDPS:{
|
|
chance = 100;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_Heal: {
|
|
switch(mercClass)
|
|
{
|
|
case TANK: {
|
|
break;
|
|
}
|
|
case HEALER:{
|
|
chance = 100;
|
|
break;
|
|
}
|
|
case MELEEDPS:{
|
|
break;
|
|
}
|
|
case CASTERDPS:{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_Root: {
|
|
switch(mercClass)
|
|
{
|
|
case TANK: {
|
|
break;
|
|
}
|
|
case HEALER:{
|
|
break;
|
|
}
|
|
case MELEEDPS:{
|
|
break;
|
|
}
|
|
case CASTERDPS:{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_Buff: {
|
|
switch(mercClass)
|
|
{
|
|
case TANK: {
|
|
break;
|
|
}
|
|
case HEALER:{
|
|
chance = IsEngaged() ? 0 : 100;
|
|
break;
|
|
}
|
|
case MELEEDPS:{
|
|
break;
|
|
}
|
|
case CASTERDPS:{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_InCombatBuff: {
|
|
switch(mercClass)
|
|
{
|
|
case TANK: {
|
|
chance = 50;
|
|
break;
|
|
}
|
|
case HEALER:{
|
|
break;
|
|
}
|
|
case MELEEDPS:{
|
|
chance = 50;
|
|
break;
|
|
}
|
|
case CASTERDPS:{
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SpellType_Escape: {
|
|
switch(mercClass)
|
|
{
|
|
case TANK: {
|
|
break;
|
|
}
|
|
case HEALER:{
|
|
break;
|
|
}
|
|
case MELEEDPS:{
|
|
chance = 100;
|
|
break;
|
|
}
|
|
case CASTERDPS:{
|
|
chance = 100;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
chance = 0;
|
|
break;
|
|
}
|
|
|
|
return chance;
|
|
}
|
|
|
|
bool Merc::CheckStance(int16 stance) {
|
|
|
|
//checks of current stance matches stances listed as valid for spell in database
|
|
//stance = 0 for all stances, stance # for only that stance & -stance# for all but that stance
|
|
if (stance == 0 || (stance > 0 && stance == GetStance()) || (stance < 0 && std::abs(stance) != GetStance())) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::list<MercSpell> Merc::GetMercSpellsBySpellType(Merc* caster, uint32 spellType) {
|
|
std::list<MercSpell> result;
|
|
|
|
if(caster && caster->AI_HasSpells()) {
|
|
std::vector<MercSpell> mercSpellList = caster->GetMercSpells();
|
|
|
|
for (int i = mercSpellList.size() - 1; i >= 0; i--) {
|
|
if (!IsValidSpell(mercSpellList[i].spellid)) {
|
|
// this is both to quit early to save cpu and to avoid casting bad spells
|
|
// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
|
|
continue;
|
|
}
|
|
|
|
if((mercSpellList[i].type & spellType) && caster->CheckStance(mercSpellList[i].stance)) {
|
|
MercSpell mercSpell;
|
|
mercSpell.spellid = mercSpellList[i].spellid;
|
|
mercSpell.stance = mercSpellList[i].stance;
|
|
mercSpell.type = mercSpellList[i].type;
|
|
mercSpell.slot = mercSpellList[i].slot;
|
|
mercSpell.proc_chance = mercSpellList[i].proc_chance;
|
|
mercSpell.time_cancast = mercSpellList[i].time_cancast;
|
|
|
|
result.push_back(mercSpell);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetFirstMercSpellBySpellType(Merc* caster, uint32 spellType) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster && caster->AI_HasSpells()) {
|
|
std::vector<MercSpell> mercSpellList = caster->GetMercSpells();
|
|
|
|
for (int i = mercSpellList.size() - 1; i >= 0; i--) {
|
|
if (!IsValidSpell(mercSpellList[i].spellid)) {
|
|
// this is both to quit early to save cpu and to avoid casting bad spells
|
|
// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
|
|
continue;
|
|
}
|
|
|
|
if((mercSpellList[i].type & spellType)
|
|
&& caster->CheckStance(mercSpellList[i].stance)
|
|
&& CheckSpellRecastTimers(caster, mercSpellList[i].spellid)) {
|
|
result.spellid = mercSpellList[i].spellid;
|
|
result.stance = mercSpellList[i].stance;
|
|
result.type = mercSpellList[i].type;
|
|
result.slot = mercSpellList[i].slot;
|
|
result.proc_chance = mercSpellList[i].proc_chance;
|
|
result.time_cancast = mercSpellList[i].time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetMercSpellBySpellID(Merc* caster, uint16 spellid) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster && caster->AI_HasSpells()) {
|
|
std::vector<MercSpell> mercSpellList = caster->GetMercSpells();
|
|
|
|
for (int i = mercSpellList.size() - 1; i >= 0; i--) {
|
|
if (!IsValidSpell(mercSpellList[i].spellid)) {
|
|
// this is both to quit early to save cpu and to avoid casting bad spells
|
|
// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
|
|
continue;
|
|
}
|
|
|
|
if((mercSpellList[i].spellid == spellid)
|
|
&& caster->CheckStance(mercSpellList[i].stance)) {
|
|
result.spellid = mercSpellList[i].spellid;
|
|
result.stance = mercSpellList[i].stance;
|
|
result.type = mercSpellList[i].type;
|
|
result.slot = mercSpellList[i].slot;
|
|
result.proc_chance = mercSpellList[i].proc_chance;
|
|
result.time_cancast = mercSpellList[i].time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::list<MercSpell> Merc::GetMercSpellsForSpellEffect(Merc* caster, int spellEffect) {
|
|
std::list<MercSpell> result;
|
|
|
|
if(caster && caster->AI_HasSpells()) {
|
|
std::vector<MercSpell> mercSpellList = caster->GetMercSpells();
|
|
|
|
for (int i = mercSpellList.size() - 1; i >= 0; i--) {
|
|
if (!IsValidSpell(mercSpellList[i].spellid)) {
|
|
// this is both to quit early to save cpu and to avoid casting bad spells
|
|
// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
|
|
continue;
|
|
}
|
|
|
|
if(IsEffectInSpell(mercSpellList[i].spellid, spellEffect) && caster->CheckStance(mercSpellList[i].stance)) {
|
|
MercSpell MercSpell;
|
|
MercSpell.spellid = mercSpellList[i].spellid;
|
|
MercSpell.stance = mercSpellList[i].stance;
|
|
MercSpell.type = mercSpellList[i].type;
|
|
MercSpell.slot = mercSpellList[i].slot;
|
|
MercSpell.proc_chance = mercSpellList[i].proc_chance;
|
|
MercSpell.time_cancast = mercSpellList[i].time_cancast;
|
|
|
|
result.push_back(MercSpell);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::list<MercSpell> Merc::GetMercSpellsForSpellEffectAndTargetType(Merc* caster, int spellEffect, SpellTargetType targetType) {
|
|
std::list<MercSpell> result;
|
|
|
|
if(caster && caster->AI_HasSpells()) {
|
|
std::vector<MercSpell> mercSpellList = caster->GetMercSpells();
|
|
|
|
for (int i = mercSpellList.size() - 1; i >= 0; i--) {
|
|
if (!IsValidSpell(mercSpellList[i].spellid)) {
|
|
// this is both to quit early to save cpu and to avoid casting bad spells
|
|
// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
|
|
continue;
|
|
}
|
|
|
|
if(IsEffectInSpell(mercSpellList[i].spellid, spellEffect) && caster->CheckStance(mercSpellList[i].stance)) {
|
|
if(spells[mercSpellList[i].spellid].target_type == targetType) {
|
|
MercSpell MercSpell;
|
|
MercSpell.spellid = mercSpellList[i].spellid;
|
|
MercSpell.stance = mercSpellList[i].stance;
|
|
MercSpell.type = mercSpellList[i].type;
|
|
MercSpell.slot = mercSpellList[i].slot;
|
|
MercSpell.proc_chance = mercSpellList[i].proc_chance;
|
|
MercSpell.time_cancast = mercSpellList[i].time_cancast;
|
|
|
|
result.push_back(MercSpell);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForVeryFastHeal(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_CurrentHP);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsVeryFastHealSpell(mercSpellListItr->spellid)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForFastHeal(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_CurrentHP);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsFastHealSpell(mercSpellListItr->spellid)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForHealOverTime(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercHoTSpellList = GetMercSpellsForSpellEffect(caster, SE_HealOverTime);
|
|
|
|
for (auto mercSpellListItr = mercHoTSpellList.begin(); mercSpellListItr != mercHoTSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsHealOverTimeSpell(mercSpellListItr->spellid)) {
|
|
|
|
if (mercSpellListItr->spellid <= 0 || mercSpellListItr->spellid >= SPDAT_RECORDS) {
|
|
// this is both to quit early to save cpu and to avoid casting bad spells
|
|
// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
|
|
continue;
|
|
}
|
|
|
|
if(CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForPercentageHeal(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster && caster->AI_HasSpells()) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_CurrentHP);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsCompleteHealSpell(mercSpellListItr->spellid)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForRegularSingleTargetHeal(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_CurrentHP);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsRegularSingleTargetHealSpell(mercSpellListItr->spellid)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetFirstMercSpellForSingleTargetHeal(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_CurrentHP);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if((IsRegularSingleTargetHealSpell(mercSpellListItr->spellid)
|
|
|| IsFastHealSpell(mercSpellListItr->spellid))
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForGroupHeal(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_CurrentHP);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsRegularGroupHealSpell(mercSpellListItr->spellid)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForGroupHealOverTime(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercHoTSpellList = GetMercSpellsForSpellEffect(caster, SE_HealOverTime);
|
|
|
|
for (auto mercSpellListItr = mercHoTSpellList.begin(); mercSpellListItr != mercHoTSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsGroupHealOverTimeSpell(mercSpellListItr->spellid)) {
|
|
|
|
if (mercSpellListItr->spellid <= 0 || mercSpellListItr->spellid >= SPDAT_RECORDS) {
|
|
// this is both to quit early to save cpu and to avoid casting bad spells
|
|
// Bad info from database can trigger this incorrectly, but that should be fixed in DB, not here
|
|
continue;
|
|
}
|
|
|
|
if(CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForGroupCompleteHeal(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_CompleteHeal);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsGroupCompleteHealSpell(mercSpellListItr->spellid)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForAETaunt(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_Taunt);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if((spells[mercSpellListItr->spellid].target_type == ST_AECaster
|
|
|| spells[mercSpellListItr->spellid].target_type == ST_AETarget
|
|
|| spells[mercSpellListItr->spellid].target_type == ST_UndeadAE)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForTaunt(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_Taunt);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if((spells[mercSpellListItr->spellid].target_type == ST_Target)
|
|
&& CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForHate(Merc* caster) {
|
|
MercSpell result;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_InstantHate);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForCure(Merc* caster, Mob *tar) {
|
|
MercSpell result;
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(!tar)
|
|
return result;
|
|
|
|
int countNeedsCured = 0;
|
|
bool isPoisoned = tar->FindType(SE_PoisonCounter);
|
|
bool isDiseased = tar->FindType(SE_DiseaseCounter);
|
|
bool isCursed = tar->FindType(SE_CurseCounter);
|
|
bool isCorrupted = tar->FindType(SE_CorruptionCounter);
|
|
|
|
if(caster && caster->AI_HasSpells()) {
|
|
std::list<MercSpell> cureList = GetMercSpellsBySpellType(caster, SpellType_Cure);
|
|
|
|
if(tar->HasGroup()) {
|
|
Group *g = tar->GetGroup();
|
|
|
|
if(g) {
|
|
for( int i = 0; i<MAX_GROUP_MEMBERS; i++) {
|
|
if(g->members[i] && !g->members[i]->qglobal) {
|
|
if(caster->GetNeedsCured(g->members[i]))
|
|
countNeedsCured++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Check for group cure first
|
|
if(countNeedsCured > 2) {
|
|
for (auto itr = cureList.begin(); itr != cureList.end(); ++itr) {
|
|
MercSpell selectedMercSpell = *itr;
|
|
|
|
if(IsGroupSpell(itr->spellid) && CheckSpellRecastTimers(caster, itr->spellid)) {
|
|
if(selectedMercSpell.spellid == 0)
|
|
continue;
|
|
|
|
if(isPoisoned && IsEffectInSpell(itr->spellid, SE_PoisonCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(isDiseased && IsEffectInSpell(itr->spellid, SE_DiseaseCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(isCursed && IsEffectInSpell(itr->spellid, SE_CurseCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(isCorrupted && IsEffectInSpell(itr->spellid, SE_CorruptionCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(IsEffectInSpell(itr->spellid, SE_DispelDetrimental)) {
|
|
spellSelected = true;
|
|
}
|
|
|
|
if(spellSelected)
|
|
{
|
|
result.spellid = itr->spellid;
|
|
result.stance = itr->stance;
|
|
result.type = itr->type;
|
|
result.slot = itr->slot;
|
|
result.proc_chance = itr->proc_chance;
|
|
result.time_cancast = itr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//no group cure for target- try to find single target spell
|
|
if(!spellSelected) {
|
|
for (auto itr = cureList.begin(); itr != cureList.end(); ++itr) {
|
|
MercSpell selectedMercSpell = *itr;
|
|
|
|
if(CheckSpellRecastTimers(caster, itr->spellid)) {
|
|
if(selectedMercSpell.spellid == 0)
|
|
continue;
|
|
|
|
if(isPoisoned && IsEffectInSpell(itr->spellid, SE_PoisonCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(isDiseased && IsEffectInSpell(itr->spellid, SE_DiseaseCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(isCursed && IsEffectInSpell(itr->spellid, SE_CurseCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(isCorrupted && IsEffectInSpell(itr->spellid, SE_CorruptionCounter)) {
|
|
spellSelected = true;
|
|
}
|
|
else if(IsEffectInSpell(itr->spellid, SE_DispelDetrimental)) {
|
|
spellSelected = true;
|
|
}
|
|
|
|
if(spellSelected)
|
|
{
|
|
result.spellid = itr->spellid;
|
|
result.stance = itr->stance;
|
|
result.type = itr->type;
|
|
result.slot = itr->slot;
|
|
result.proc_chance = itr->proc_chance;
|
|
result.time_cancast = itr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForStun(Merc* caster) {
|
|
MercSpell result;
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsForSpellEffect(caster, SE_Stun);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForAENuke(Merc* caster, Mob* tar) {
|
|
MercSpell result;
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
uint8 initialCastChance = 0;
|
|
uint8 castChanceFalloff = 75;
|
|
|
|
switch(caster->GetStance())
|
|
{
|
|
case Stance::AEBurn:
|
|
initialCastChance = 50;
|
|
break;
|
|
case Stance::Balanced:
|
|
initialCastChance = 25;
|
|
break;
|
|
case Stance::Burn:
|
|
initialCastChance = 0;
|
|
break;
|
|
}
|
|
|
|
//check of we even want to cast an AE nuke
|
|
if(zone->random.Roll(initialCastChance)) {
|
|
|
|
result = GetBestMercSpellForAERainNuke(caster, tar);
|
|
|
|
//check if we have a spell & allow for other AE nuke types
|
|
if(result.spellid == 0 && zone->random.Roll(castChanceFalloff)) {
|
|
|
|
result = GetBestMercSpellForPBAENuke(caster, tar);
|
|
|
|
//check if we have a spell & allow for other AE nuke types
|
|
if(result.spellid == 0 && zone->random.Roll(castChanceFalloff)) {
|
|
|
|
result = GetBestMercSpellForTargetedAENuke(caster, tar);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForTargetedAENuke(Merc* caster, Mob* tar) {
|
|
MercSpell result;
|
|
int castChance = 50; //used to cycle through multiple spells (first has 50% overall chance, 2nd has 25%, etc.)
|
|
int numTargetsCheck = 1; //used to check for min number of targets to use AE
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if (!caster) {
|
|
return result;
|
|
}
|
|
|
|
switch(caster->GetStance())
|
|
{
|
|
case Stance::AEBurn:
|
|
numTargetsCheck = 1;
|
|
break;
|
|
case Stance::Balanced:
|
|
case Stance::Burn:
|
|
numTargetsCheck = 2;
|
|
break;
|
|
}
|
|
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsBySpellType(caster, SpellType_Nuke);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsAENukeSpell(mercSpellListItr->spellid) && !IsAERainNukeSpell(mercSpellListItr->spellid)
|
|
&& !IsPBAENukeSpell(mercSpellListItr->spellid) && CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
uint8 numTargets = 0;
|
|
if(CheckAENuke(caster, tar, mercSpellListItr->spellid, numTargets)) {
|
|
if(numTargets >= numTargetsCheck && zone->random.Roll(castChance)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForPBAENuke(Merc* caster, Mob* tar) {
|
|
MercSpell result;
|
|
int castChance = 50; //used to cycle through multiple spells (first has 50% overall chance, 2nd has 25%, etc.)
|
|
int numTargetsCheck = 1; //used to check for min number of targets to use AE
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if (!caster) {
|
|
return result;
|
|
}
|
|
|
|
switch(caster->GetStance())
|
|
{
|
|
case Stance::AEBurn:
|
|
numTargetsCheck = 2;
|
|
break;
|
|
case Stance::Balanced:
|
|
case Stance::Burn:
|
|
numTargetsCheck = 3;
|
|
break;
|
|
}
|
|
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsBySpellType(caster, SpellType_Nuke);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsPBAENukeSpell(mercSpellListItr->spellid) && CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
uint8 numTargets = 0;
|
|
if(CheckAENuke(caster, caster, mercSpellListItr->spellid, numTargets)) {
|
|
if(numTargets >= numTargetsCheck && zone->random.Roll(castChance)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForAERainNuke(Merc* caster, Mob* tar) {
|
|
MercSpell result;
|
|
int castChance = 50; //used to cycle through multiple spells (first has 50% overall chance, 2nd has 25%, etc.)
|
|
int numTargetsCheck = 1; //used to check for min number of targets to use AE
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if (!caster) {
|
|
return result;
|
|
}
|
|
|
|
switch(caster->GetStance())
|
|
{
|
|
case Stance::AEBurn:
|
|
numTargetsCheck = 1;
|
|
break;
|
|
case Stance::Balanced:
|
|
case Stance::Burn:
|
|
numTargetsCheck = 2;
|
|
break;
|
|
}
|
|
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsBySpellType(caster, SpellType_Nuke);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsAERainNukeSpell(mercSpellListItr->spellid) && zone->random.Roll(castChance) && CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
uint8 numTargets = 0;
|
|
if(CheckAENuke(caster, tar, mercSpellListItr->spellid, numTargets)) {
|
|
if(numTargets >= numTargetsCheck) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForNuke(Merc* caster) {
|
|
MercSpell result;
|
|
int castChance = 50; //used to cycle through multiple spells (first has 50% overall chance, 2nd has 25%, etc.)
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(caster) {
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsBySpellType(caster, SpellType_Nuke);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
if(IsPureNukeSpell(mercSpellListItr->spellid) && !IsAENukeSpell(mercSpellListItr->spellid)
|
|
&& zone->random.Roll(castChance) && CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
MercSpell Merc::GetBestMercSpellForNukeByTargetResists(Merc* caster, Mob* target) {
|
|
MercSpell result;
|
|
bool spellSelected = false;
|
|
|
|
result.spellid = 0;
|
|
result.stance = 0;
|
|
result.type = 0;
|
|
result.slot = 0;
|
|
result.proc_chance = 0;
|
|
result.time_cancast = 0;
|
|
|
|
if(!target)
|
|
return result;
|
|
|
|
if(caster) {
|
|
const int lureResisValue = -100;
|
|
const int maxTargetResistValue = 300;
|
|
bool selectLureNuke = false;
|
|
|
|
if((target->GetMR() > maxTargetResistValue) && (target->GetCR() > maxTargetResistValue) && (target->GetFR() > maxTargetResistValue))
|
|
selectLureNuke = true;
|
|
|
|
std::list<MercSpell> mercSpellList = GetMercSpellsBySpellType(caster, SpellType_Nuke);
|
|
|
|
for (auto mercSpellListItr = mercSpellList.begin(); mercSpellListItr != mercSpellList.end();
|
|
++mercSpellListItr) {
|
|
// Assuming all the spells have been loaded into this list by level and in descending order
|
|
|
|
if(IsPureNukeSpell(mercSpellListItr->spellid) && !IsAENukeSpell(mercSpellListItr->spellid) && CheckSpellRecastTimers(caster, mercSpellListItr->spellid)) {
|
|
if(selectLureNuke && (spells[mercSpellListItr->spellid].resist_difficulty < lureResisValue)) {
|
|
spellSelected = true;
|
|
}
|
|
else {
|
|
if(((target->GetMR() < target->GetCR()) || (target->GetMR() < target->GetFR())) && (GetSpellResistType(mercSpellListItr->spellid) == RESIST_MAGIC)
|
|
&& (spells[mercSpellListItr->spellid].resist_difficulty > lureResisValue))
|
|
{
|
|
spellSelected = true;
|
|
}
|
|
else if(((target->GetCR() < target->GetMR()) || (target->GetCR() < target->GetFR())) && (GetSpellResistType(mercSpellListItr->spellid) == RESIST_COLD)
|
|
&& (spells[mercSpellListItr->spellid].resist_difficulty > lureResisValue))
|
|
{
|
|
spellSelected = true;
|
|
}
|
|
else if(((target->GetFR() < target->GetCR()) || (target->GetFR() < target->GetMR())) && (GetSpellResistType(mercSpellListItr->spellid) == RESIST_FIRE)
|
|
&& (spells[mercSpellListItr->spellid].resist_difficulty > lureResisValue))
|
|
{
|
|
spellSelected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(spellSelected) {
|
|
result.spellid = mercSpellListItr->spellid;
|
|
result.stance = mercSpellListItr->stance;
|
|
result.type = mercSpellListItr->type;
|
|
result.slot = mercSpellListItr->slot;
|
|
result.proc_chance = mercSpellListItr->proc_chance;
|
|
result.time_cancast = mercSpellListItr->time_cancast;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool Merc::GetNeedsCured(Mob *tar) {
|
|
bool needCured = false;
|
|
|
|
if(tar) {
|
|
if(tar->FindType(SE_PoisonCounter) || tar->FindType(SE_DiseaseCounter) || tar->FindType(SE_CurseCounter) || tar->FindType(SE_CorruptionCounter)) {
|
|
uint32 buff_count = tar->GetMaxTotalSlots();
|
|
int buffsWithCounters = 0;
|
|
needCured = true;
|
|
|
|
for (unsigned int j = 0; j < buff_count; j++) {
|
|
if (IsValidSpell(tar->GetBuffs()[j].spellid)) {
|
|
if(CalculateCounters(tar->GetBuffs()[j].spellid) > 0) {
|
|
buffsWithCounters++;
|
|
|
|
if(buffsWithCounters == 1 && (tar->GetBuffs()[j].ticsremaining < 2 || (int32)((tar->GetBuffs()[j].ticsremaining * 6) / tar->GetBuffs()[j].counters) < 2)) {
|
|
// Spell has ticks remaining but may have too many counters to cure in the time remaining;
|
|
// We should try to just wait it out. Could spend entire time trying to cure spell instead of healing, buffing, etc.
|
|
// Since this is the first buff with counters, don't try to cure. Cure spell will be wasted, as cure will try to
|
|
// remove counters from the first buff that has counters remaining.
|
|
needCured = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return needCured;
|
|
}
|
|
|
|
void Merc::MercGroupSay(Mob *speaker, const char *msg, ...)
|
|
{
|
|
|
|
char buf[1000];
|
|
va_list ap;
|
|
|
|
va_start(ap, msg);
|
|
vsnprintf(buf, 1000, msg, ap);
|
|
va_end(ap);
|
|
|
|
if(speaker->HasGroup()) {
|
|
Group *g = speaker->GetGroup();
|
|
|
|
if(g)
|
|
g->GroupMessage(speaker->CastToMob(), Language::CommonTongue, Language::MaxValue, buf);
|
|
}
|
|
}
|
|
|
|
bool Merc::UseDiscipline(int32 spell_id, int32 target) {
|
|
// Dont let client waste a reuse timer if they can't use the disc
|
|
if (IsStunned() || IsFeared() || IsMezzed() || IsAmnesiad())
|
|
{
|
|
return(false);
|
|
}
|
|
|
|
//make sure we can use it..
|
|
if(!IsValidSpell(spell_id)) {
|
|
return(false);
|
|
}
|
|
|
|
const SPDat_Spell_Struct &spell = spells[spell_id];
|
|
|
|
if(spell.recast_time > 0)
|
|
{
|
|
if(CheckDisciplineRecastTimers(this, spell_id, spells[spell_id].timer_id)) {
|
|
if(spells[spell_id].timer_id > 0) {
|
|
SetDisciplineRecastTimer(spells[spell_id].timer_id, spell_id, spell.recast_time);
|
|
}
|
|
|
|
SetSpellTimeCanCast(spell_id, spells[spell_id].recast_time);
|
|
}
|
|
else {
|
|
return(false);
|
|
}
|
|
}
|
|
|
|
if(GetEndurance() > spell.endurance_cost) {
|
|
SetEndurance(GetEndurance() - spell.endurance_cost);
|
|
} else {
|
|
//too fatigued to use this skill right now.
|
|
return(false);
|
|
}
|
|
|
|
if(IsCasting())
|
|
InterruptSpell();
|
|
|
|
CastSpell(spell_id, target, EQ::spells::CastingSlot::Discipline);
|
|
|
|
return(true);
|
|
}
|
|
|
|
void Merc::SetSpellRecastTimer(uint16 timer_id, uint16 spellid, uint32 recast_delay) {
|
|
if(timer_id > 0) {
|
|
MercTimer timer;
|
|
timer.timerid = timer_id;
|
|
timer.timertype = 1;
|
|
timer.spellid = spellid;
|
|
timer.time_cancast = Timer::GetCurrentTime() + recast_delay;
|
|
timers[timer_id] = timer;
|
|
}
|
|
}
|
|
|
|
int32 Merc::GetSpellRecastTimer(Merc *caster, uint16 timer_id) {
|
|
int32 result = 0;
|
|
if(caster && timer_id > 0) {
|
|
if(caster->timers.find(timer_id) != caster->timers.end()) {
|
|
result = caster->timers[timer_id].time_cancast;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool Merc::CheckSpellRecastTimers(Merc *caster, uint16 spell_id) {
|
|
if(caster) {
|
|
MercSpell mercSpell = GetMercSpellBySpellID(caster, spell_id);
|
|
if(mercSpell.spellid > 0 && mercSpell.time_cancast < Timer::GetCurrentTime()) { //checks spell recast
|
|
if(GetSpellRecastTimer(caster, spells[spell_id].timer_id) < Timer::GetCurrentTime()) { //checks for spells on the same timer
|
|
return true; //can cast spell
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Merc::SetDisciplineRecastTimer(uint16 timer_id, uint16 spellid, uint32 recast_delay) {
|
|
if(timer_id > 0) {
|
|
MercTimer timer;
|
|
timer.timerid = timer_id;
|
|
timer.timertype = 2;
|
|
timer.spellid = spellid;
|
|
timer.time_cancast = Timer::GetCurrentTime() + recast_delay;
|
|
timers[timer_id] = timer;
|
|
}
|
|
}
|
|
|
|
int32 Merc::GetDisciplineRecastTimer(Merc *caster, uint16 timer_id) {
|
|
int32 result = 0;
|
|
if(caster && timer_id > 0) {
|
|
if(caster->timers.find(timer_id) != caster->timers.end()) {
|
|
result = caster->timers[timer_id].time_cancast;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int32 Merc::GetDisciplineRemainingTime(Merc *caster, uint16 timer_id) {
|
|
int32 result = 0;
|
|
if(caster && timer_id > 0) {
|
|
int32 time_cancast = GetDisciplineRecastTimer(caster, timer_id);
|
|
if(time_cancast > Timer::GetCurrentTime())
|
|
result = time_cancast - Timer::GetCurrentTime();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool Merc::CheckDisciplineRecastTimers(Merc *caster, uint16 spell_id, uint16 timer_id) {
|
|
if(caster) {
|
|
MercSpell mercSpell = GetMercSpellBySpellID(caster, spell_id);
|
|
if(mercSpell.spellid > 0 && mercSpell.time_cancast < Timer::GetCurrentTime()) { //checks spell recast
|
|
if(timer_id > 0 && !(GetDisciplineRecastTimer(caster, timer_id) < Timer::GetCurrentTime())) { //checks for spells on the same timer
|
|
return false; //can't cast spell
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Merc::SetSpellTimeCanCast(uint16 spellid, uint32 recast_delay) {
|
|
for (int i = 0; i < merc_spells.size(); i++) {
|
|
if(merc_spells[i].spellid == spellid) {
|
|
merc_spells[i].time_cancast = Timer::GetCurrentTime() + recast_delay;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Merc::CheckTaunt() {
|
|
Mob* tar = GetTarget();
|
|
//Only taunt if we are not top on target's hate list
|
|
//This ensures we have taunt available to regain aggro if needed
|
|
if(tar && tar->GetHateTop() && tar->GetHateTop() != this) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Merc::CheckAETaunt() {
|
|
//need to check area for mobs needing taunted
|
|
MercSpell mercSpell = GetBestMercSpellForAETaunt(this);
|
|
uint8 result = 0;
|
|
|
|
if(mercSpell.spellid != 0) {
|
|
|
|
std::list<NPC*> npc_list;
|
|
entity_list.GetNPCList(npc_list);
|
|
|
|
for (auto itr = npc_list.begin(); itr != npc_list.end(); ++itr) {
|
|
NPC* npc = *itr;
|
|
float dist = DistanceSquaredNoZ(m_Position, npc->GetPosition());
|
|
int range = GetActSpellRange(mercSpell.spellid, spells[mercSpell.spellid].range);
|
|
range *= range;
|
|
|
|
if(dist <= range) {
|
|
if(!npc->IsMezzed()) {
|
|
if(HasGroup()) {
|
|
Group* g = GetGroup();
|
|
|
|
if(g) {
|
|
for(int i = 0; i < g->GroupCount(); i++) {
|
|
//if(npc->IsOnHatelist(g->members[i]) && g->members[i]->GetTarget() != npc && g->members[i]->IsEngaged()) {
|
|
if(GetTarget() != npc && g->members[i] && g->members[i]->GetTarget() != npc && npc->IsOnHatelist(g->members[i])) {
|
|
result++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(result >= 1) {
|
|
Log(Logs::General, Logs::Mercenaries, "%s: Attempting AE Taunt", GetCleanName());
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Corpse* Merc::GetGroupMemberCorpse() {
|
|
Corpse* corpse = nullptr;
|
|
|
|
if(HasGroup()) {
|
|
Group* g = GetGroup();
|
|
|
|
if(g) {
|
|
for(int i = 0; i < g->GroupCount(); i++) {
|
|
if(g->members[i] && g->members[i]->IsClient()) {
|
|
corpse = entity_list.GetCorpseByOwnerWithinRange(g->members[i]->CastToClient(), this, RuleI(Mercs, ResurrectRadius));
|
|
|
|
if(corpse && !corpse->IsRezzed()) {
|
|
return corpse;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool Merc::TryHide() {
|
|
if(GetClass() != MELEEDPS) {
|
|
return false;
|
|
}
|
|
|
|
//don't hide if already hidden
|
|
if(hidden == true) {
|
|
return false;
|
|
}
|
|
|
|
auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct));
|
|
SpawnAppearance_Struct* sa_out = (SpawnAppearance_Struct*)outapp->pBuffer;
|
|
sa_out->spawn_id = GetID();
|
|
sa_out->type = 0x03;
|
|
sa_out->parameter = 1;
|
|
entity_list.QueueClients(this, outapp, true);
|
|
safe_delete(outapp);
|
|
hidden = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
//Checks if Merc still has confidence. Can be checked to begin fleeing, or to regain confidence after confidence loss - true = confident, false = confidence loss
|
|
bool Merc::CheckConfidence() {
|
|
bool result = true;
|
|
int ConfidenceLossChance = 0;
|
|
float ConfidenceCheck = 0;
|
|
int ConfidenceRating = 2 * GetProficiencyID();
|
|
|
|
std::list<NPC*> npc_list;
|
|
entity_list.GetNPCList(npc_list);
|
|
|
|
for (auto itr = npc_list.begin(); itr != npc_list.end(); ++itr) {
|
|
NPC* mob = *itr;
|
|
float ConRating = 1.0;
|
|
int CurrentCon = 0;
|
|
|
|
if(!mob) continue;
|
|
|
|
if(!mob->IsEngaged()) continue;
|
|
|
|
if(mob->IsFeared() || mob->IsMezzed() || mob->IsStunned() || mob->IsRooted() || mob->IsCharmed()) continue;
|
|
|
|
if(!mob->CheckAggro(this)) continue;
|
|
|
|
float AggroRange = mob->GetAggroRange();
|
|
|
|
// Square it because we will be using DistNoRoot
|
|
|
|
AggroRange = AggroRange * AggroRange;
|
|
|
|
if(DistanceSquared(m_Position, mob->GetPosition()) > AggroRange) continue;
|
|
|
|
CurrentCon = GetLevelCon(mob->GetLevel());
|
|
switch(CurrentCon) {
|
|
|
|
|
|
case ConsiderColor::Gray: {
|
|
ConRating = 0;
|
|
break;
|
|
}
|
|
|
|
case ConsiderColor::Green: {
|
|
ConRating = 0.1;
|
|
break;
|
|
}
|
|
|
|
case ConsiderColor::LightBlue: {
|
|
ConRating = 0.2;
|
|
break;
|
|
}
|
|
|
|
case ConsiderColor::DarkBlue: {
|
|
ConRating = 0.6;
|
|
break;
|
|
}
|
|
|
|
case ConsiderColor::White: {
|
|
ConRating = 1.0;
|
|
break;
|
|
}
|
|
|
|
case ConsiderColor::Yellow: {
|
|
ConRating = 1.2;
|
|
break;
|
|
}
|
|
|
|
case ConsiderColor::Red: {
|
|
ConRating = 1.5;
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
ConRating = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ConfidenceCheck += ConRating;
|
|
}
|
|
|
|
if(ConfidenceRating < ConfidenceCheck) {
|
|
ConfidenceLossChance = 25 - ( 5 * (GetTierID() - 1));
|
|
}
|
|
|
|
if(zone->random.Roll(ConfidenceLossChance)) {
|
|
result = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void Merc::MercMeditate(bool isSitting) {
|
|
// Don't try to meditate if engaged or dead
|
|
if (IsEngaged() || GetAppearance() == eaDead)
|
|
{
|
|
return;
|
|
}
|
|
if (isSitting) {
|
|
// If the merc is a caster and has less than 99% mana while its not engaged, he needs to sit to meditate
|
|
if (GetManaRatio() < 99.0f)
|
|
{
|
|
if(!IsSitting()) {
|
|
Sit();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsSitting()) {
|
|
Stand();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsSitting()) {
|
|
Stand();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void Merc::Sit() {
|
|
if(IsMoving()) {
|
|
moved = false;
|
|
// SetHeading(CalculateHeadingToTarget(GetTarget()->GetX(), GetTarget()->GetY()));
|
|
SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0);
|
|
SetMoving(false);
|
|
}
|
|
|
|
SetAppearance(eaSitting);
|
|
}
|
|
|
|
void Merc::Stand() {
|
|
SetAppearance(eaStanding);
|
|
}
|
|
|
|
bool Merc::IsSitting() const {
|
|
bool result = false;
|
|
|
|
if(GetAppearance() == eaSitting && !IsMoving())
|
|
result = true;
|
|
|
|
return result;
|
|
}
|
|
|
|
bool Merc::IsStanding() {
|
|
bool result = false;
|
|
|
|
if(GetAppearance() == eaStanding)
|
|
result = true;
|
|
|
|
return result;
|
|
}
|
|
|
|
float Merc::GetMaxMeleeRangeToTarget(Mob* target) {
|
|
float result = 0;
|
|
|
|
if (target) {
|
|
float size_mod = GetSize();
|
|
float other_size_mod = target->GetSize();
|
|
|
|
if (GetRace() == Race::LavaDragon || GetRace() == Race::Wurm || GetRace() == Race::GhostDragon) //For races with a fixed size
|
|
{
|
|
size_mod = 60.0f;
|
|
} else if (size_mod < 6.0) {
|
|
size_mod = 8.0f;
|
|
}
|
|
|
|
if (target->GetRace() == Race::LavaDragon || target->GetRace() == Race::Wurm || target->GetRace() == Race::GhostDragon) //For races with a fixed size
|
|
{
|
|
other_size_mod = 60.0f;
|
|
} else if (other_size_mod < 6.0) {
|
|
other_size_mod = 8.0f;
|
|
}
|
|
|
|
if (other_size_mod > size_mod) {
|
|
size_mod = other_size_mod;
|
|
}
|
|
|
|
// this could still use some work, but for now it's an improvement....
|
|
|
|
if (size_mod > 29) {
|
|
size_mod *= size_mod;
|
|
} else if (size_mod > 19) {
|
|
size_mod *= size_mod * 2;
|
|
} else {
|
|
size_mod *= size_mod * 4;
|
|
}
|
|
|
|
// prevention of ridiculously sized hit boxes
|
|
if (size_mod > 10000)
|
|
size_mod = size_mod / 7;
|
|
|
|
result = size_mod;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void Merc::DoClassAttacks(Mob *target) {
|
|
if(target == nullptr)
|
|
return; //gotta have a target for all these
|
|
|
|
bool ca_time = classattack_timer.Check(false);
|
|
|
|
//only check attack allowed if we are going to do something
|
|
if(ca_time && !IsAttackAllowed(target))
|
|
return;
|
|
|
|
if(!ca_time)
|
|
return;
|
|
|
|
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
|
|
bool did_attack = false;
|
|
//class specific stuff...
|
|
switch(GetClass()) {
|
|
case MELEEDPS:
|
|
if(level >= 10) {
|
|
reuse = BackstabReuseTime * 1000;
|
|
TryBackstab(target, reuse);
|
|
did_attack = true;
|
|
}
|
|
break;
|
|
case TANK:{
|
|
if(level >= RuleI(Combat, NPCBashKickLevel)){
|
|
if(zone->random.Int(0, 100) > 25) //tested on live, warrior mobs both kick and bash, kick about 75% of the time, casting doesn't seem to make a difference.
|
|
{
|
|
DoAnim(animKick, 0, false);
|
|
int64 dmg = GetBaseSkillDamage(EQ::skills::SkillKick);
|
|
|
|
if (GetWeaponDamage(target, (const EQ::ItemData*)nullptr) <= 0)
|
|
dmg = DMG_INVULNERABLE;
|
|
|
|
reuse = KickReuseTime * 1000;
|
|
DoSpecialAttackDamage(target, EQ::skills::SkillKick, dmg, 1, -1, reuse);
|
|
did_attack = true;
|
|
}
|
|
else
|
|
{
|
|
DoAnim(animTailRake, 0, false);
|
|
int64 dmg = GetBaseSkillDamage(EQ::skills::SkillBash);
|
|
|
|
if (GetWeaponDamage(target, (const EQ::ItemData*)nullptr) <= 0)
|
|
dmg = DMG_INVULNERABLE;
|
|
|
|
reuse = BashReuseTime * 1000;
|
|
DoSpecialAttackDamage(target, EQ::skills::SkillBash, dmg, 1, -1, reuse);
|
|
did_attack = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
classattack_timer.Start(reuse / HasteModifier);
|
|
}
|
|
|
|
bool Merc::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, bool IsFromSpell, ExtraAttackOptions *opts)
|
|
{
|
|
if (!other) {
|
|
SetTarget(nullptr);
|
|
LogError("A null Mob object was passed to Merc::Attack() for evaluation!");
|
|
return false;
|
|
}
|
|
|
|
return NPC::Attack(other, Hand, bRiposte, IsStrikethrough, IsFromSpell, opts);
|
|
}
|
|
|
|
void Merc::Damage(Mob* other, int64 damage, uint16 spell_id, EQ::skills::SkillType attack_skill, bool avoidable, int8 buffslot, bool iBuffTic, eSpecialAttacks special)
|
|
{
|
|
if(IsDead() || IsCorpse())
|
|
return;
|
|
|
|
if(spell_id==0)
|
|
spell_id = SPELL_UNKNOWN;
|
|
|
|
NPC::Damage(other, damage, spell_id, attack_skill, avoidable, buffslot, iBuffTic, special);
|
|
|
|
//Not needed since we're using NPC damage.
|
|
//CommonDamage(other, damage, spell_id, attack_skill, avoidable, buffslot, iBuffTic);
|
|
}
|
|
|
|
bool Merc::FindTarget() {
|
|
bool found = false;
|
|
Mob* target = GetHateTop();
|
|
|
|
if(target) {
|
|
found = true;
|
|
SetTarget(target);
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
void Merc::SetTarget(Mob* mob) {
|
|
NPC::SetTarget(mob);
|
|
}
|
|
|
|
Mob* Merc::GetOwnerOrSelf() {
|
|
Mob* Result = nullptr;
|
|
|
|
if(GetMercenaryOwner())
|
|
Result = GetMercenaryOwner();
|
|
else
|
|
Result = this;
|
|
|
|
return Result;
|
|
}
|
|
|
|
bool Merc::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillType attack_skill, uint8 killed_by, bool is_buff_tic)
|
|
{
|
|
if (!NPC::Death(killer_mob, damage, spell, attack_skill)) {
|
|
return false;
|
|
}
|
|
|
|
Save();
|
|
|
|
// If client is in zone, suspend merc, else depop it.
|
|
if (!Suspend()) {
|
|
Depop();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Client* Merc::GetMercenaryOwner() {
|
|
Client* mercOwner = nullptr;
|
|
|
|
if(GetOwner())
|
|
{
|
|
if(GetOwner()->IsClient())
|
|
{
|
|
mercOwner = GetOwner()->CastToClient();
|
|
}
|
|
}
|
|
|
|
return mercOwner;
|
|
}
|
|
|
|
Mob* Merc::GetOwner() {
|
|
Mob* Result = nullptr;
|
|
|
|
Result = entity_list.GetMob(GetOwnerID());
|
|
|
|
if(!Result) {
|
|
SetOwnerID(0);
|
|
}
|
|
|
|
return Result->CastToMob();
|
|
}
|
|
|
|
const char* Merc::GetRandomName(){
|
|
// creates up to a 10 char name
|
|
static char name[17];
|
|
char vowels[18]="aeiouyaeiouaeioe";
|
|
char cons[48]="bcdfghjklmnpqrstvwxzybcdgklmnprstvwbcdgkpstrkd";
|
|
char rndname[17]="\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
|
char paircons[33]="ngrkndstshthphsktrdrbrgrfrclcr";
|
|
bool valid = false;
|
|
|
|
while(!valid) {
|
|
int rndnum=zone->random.Int(0, 75),n=1;
|
|
bool dlc=false;
|
|
bool vwl=false;
|
|
bool dbl=false;
|
|
if (rndnum>63)
|
|
{ // rndnum is 0 - 75 where 64-75 is cons pair, 17-63 is cons, 0-16 is vowel
|
|
rndnum=(rndnum-61)*2; // name can't start with "ng" "nd" or "rk"
|
|
rndname[0]=paircons[rndnum];
|
|
rndname[1]=paircons[rndnum+1];
|
|
n=2;
|
|
}
|
|
else if (rndnum>16)
|
|
{
|
|
rndnum-=17;
|
|
rndname[0]=cons[rndnum];
|
|
}
|
|
else
|
|
{
|
|
rndname[0]=vowels[rndnum];
|
|
vwl=true;
|
|
}
|
|
int namlen=zone->random.Int(5, 10);
|
|
for (int i=n;i<namlen;i++)
|
|
{
|
|
dlc=false;
|
|
if (vwl) //last char was a vowel
|
|
{ // so pick a cons or cons pair
|
|
rndnum=zone->random.Int(0, 62);
|
|
if (rndnum>46)
|
|
{ // pick a cons pair
|
|
if (i>namlen-3) // last 2 chars in name?
|
|
{ // name can only end in cons pair "rk" "st" "sh" "th" "ph" "sk" "nd" or "ng"
|
|
rndnum=zone->random.Int(0, 7)*2;
|
|
}
|
|
else
|
|
{ // pick any from the set
|
|
rndnum=(rndnum-47)*2;
|
|
}
|
|
rndname[i]=paircons[rndnum];
|
|
rndname[i+1]=paircons[rndnum+1];
|
|
dlc=true; // flag keeps second letter from being doubled below
|
|
i+=1;
|
|
}
|
|
else
|
|
{ // select a single cons
|
|
rndname[i]=cons[rndnum];
|
|
}
|
|
}
|
|
else
|
|
{ // select a vowel
|
|
rndname[i]=vowels[zone->random.Int(0, 16)];
|
|
}
|
|
vwl=!vwl;
|
|
if (!dbl && !dlc)
|
|
{ // one chance at double letters in name
|
|
if (!zone->random.Int(0, i+9)) // chances decrease towards end of name
|
|
{
|
|
rndname[i+1]=rndname[i];
|
|
dbl=true;
|
|
i+=1;
|
|
}
|
|
}
|
|
}
|
|
|
|
rndname[0]=toupper(rndname[0]);
|
|
|
|
if(!database.CheckNameFilter(rndname)) {
|
|
valid = false;
|
|
}
|
|
else if(rndname[0] < 'A' && rndname[0] > 'Z') {
|
|
//name must begin with an upper-case letter.
|
|
valid = false;
|
|
}
|
|
else if (!database.IsNameUsed(rndname)) {
|
|
valid = true;
|
|
}
|
|
else {
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
memset(name, 0, 17);
|
|
strcpy(name, rndname);
|
|
return name;
|
|
}
|
|
|
|
bool Merc::LoadMercenarySpells() {
|
|
// loads mercs spells into list
|
|
merc_spells.clear();
|
|
|
|
std::list<MercSpellEntry> spellList = zone->merc_spells_list[GetClass()];
|
|
|
|
if (spellList.size() == 0) {
|
|
AIautocastspell_timer->Disable();
|
|
return false;
|
|
}
|
|
|
|
uint8 proficiency_id = GetProficiencyID();
|
|
int16 attack_proc_spell = -1;
|
|
int8 proc_chance = 0;
|
|
|
|
for (auto mercSpellEntryItr = spellList.begin(); mercSpellEntryItr != spellList.end(); ++mercSpellEntryItr) {
|
|
if (proficiency_id == mercSpellEntryItr->proficiencyid && GetLevel() >= mercSpellEntryItr->minlevel && GetLevel() <= mercSpellEntryItr->maxlevel && mercSpellEntryItr->spellid > 0) {
|
|
MercSpell mercSpell;
|
|
|
|
mercSpell.spellid = mercSpellEntryItr->spellid;
|
|
mercSpell.type = mercSpellEntryItr->type;
|
|
mercSpell.stance = mercSpellEntryItr->stance;
|
|
mercSpell.slot = mercSpellEntryItr->slot;
|
|
mercSpell.proc_chance = mercSpellEntryItr->proc_chance;
|
|
mercSpell.time_cancast = 0;
|
|
|
|
merc_spells.push_back(mercSpell);
|
|
|
|
if(mercSpellEntryItr->proc_chance > 0)
|
|
AddProcToWeapon(mercSpellEntryItr->spellid, true, mercSpellEntryItr->proc_chance);
|
|
}
|
|
}
|
|
std::sort(merc_spells.begin(), merc_spells.end(), [](const MercSpell& a, const MercSpell& b) {
|
|
return a.slot > b.slot;
|
|
});
|
|
|
|
if (merc_spells.empty())
|
|
AIautocastspell_timer->Disable();
|
|
else {
|
|
HasAISpell = true;
|
|
AIautocastspell_timer->Trigger();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Merc::Save() {
|
|
|
|
if(database.SaveMercenary(this)){
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Merc* Merc::LoadMercenary(Client *c, MercTemplate* merc_template, uint32 merchant_id, bool updateFromDB) {
|
|
|
|
if(c)
|
|
{
|
|
if(c->GetMercenaryID())
|
|
{
|
|
merc_template = zone->GetMercTemplate(c->GetMercInfo().MercTemplateID);
|
|
}
|
|
}
|
|
|
|
//get mercenary data
|
|
if(merc_template)
|
|
{
|
|
//TODO: Maybe add a way of updating client merc stats in a seperate function? like, for example, on leveling up.
|
|
|
|
const NPCType* npc_type_to_copy = nullptr;
|
|
if (c) {
|
|
npc_type_to_copy = content_db.GetMercenaryType(merc_template->MercNPCID, merc_template->RaceID, c->GetLevel());
|
|
}
|
|
|
|
if(npc_type_to_copy != nullptr)
|
|
{
|
|
//This is actually a very terrible method of assigning stats, and should be changed at some point. See the comment in merc's deconstructor.
|
|
auto npc_type = new NPCType;
|
|
memset(npc_type, 0, sizeof(NPCType));
|
|
memcpy(npc_type, npc_type_to_copy, sizeof(NPCType));
|
|
if(c && !updateFromDB)
|
|
{
|
|
if(c->GetMercInfo().merc_name[0] == 0)
|
|
{
|
|
snprintf(c->GetMercInfo().merc_name, 64, "%s", GetRandomName()); //sanity check.
|
|
}
|
|
snprintf(npc_type->name, 64, "%s", c->GetMercInfo().merc_name);
|
|
}
|
|
|
|
npc_type->race = merc_template->RaceID;
|
|
|
|
// Use the Gender and Size of the Merchant if possible
|
|
uint8 tmpgender = Gender::Male;
|
|
float tmpsize = 6.0f;
|
|
if(merchant_id > 0)
|
|
{
|
|
NPC* tar = entity_list.GetNPCByID(merchant_id);
|
|
if(tar)
|
|
{
|
|
tmpgender = tar->GetGender();
|
|
tmpsize = tar->GetSize();
|
|
}
|
|
else
|
|
{
|
|
tmpgender = Mob::GetDefaultGender(npc_type->race, c->GetMercInfo().Gender);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
tmpgender = c->GetMercInfo().Gender;
|
|
tmpsize = c->GetMercInfo().MercSize;
|
|
}
|
|
|
|
std::string tmp_lastname = c->GetName();
|
|
tmp_lastname += "'s Mercenary";
|
|
|
|
// not sure what to do if too long
|
|
if (tmp_lastname.length() < sizeof(npc_type->lastname))
|
|
strn0cpy(npc_type->lastname, tmp_lastname.c_str(), sizeof(npc_type->lastname));
|
|
npc_type->gender = tmpgender;
|
|
npc_type->size = tmpsize;
|
|
npc_type->loottable_id = 0; // Loottable has to be 0, otherwise we'll be leavin' some corpses!
|
|
npc_type->npc_id = 0; //NPC ID has to be 0, otherwise db gets all confuzzled.
|
|
npc_type->class_ = merc_template->ClassID;
|
|
npc_type->maxlevel = 0; //We should hard-set this to override scalerate's functionality in the NPC class when it is constructed.
|
|
npc_type->no_target_hotkey = 1;
|
|
|
|
auto merc = new Merc(npc_type, c->GetX(), c->GetY(), c->GetZ(), 0);
|
|
merc->GiveNPCTypeData(npc_type); // for clean up, works a bit like pets
|
|
|
|
if(merc)
|
|
{
|
|
merc->SetMercData( merc_template->MercTemplateID );
|
|
database.LoadMercenaryEquipment(merc);
|
|
merc->UpdateMercStats(c, true);
|
|
|
|
if(updateFromDB)
|
|
{
|
|
database.LoadCurrentMercenary(c);
|
|
|
|
merc->SetMercID(c->GetMercInfo().mercid);
|
|
snprintf(merc->name, 64, "%s", c->GetMercInfo().merc_name);
|
|
merc->SetSuspended(c->GetMercInfo().IsSuspended);
|
|
merc->gender = c->GetMercInfo().Gender;
|
|
merc->size = c->GetMercInfo().MercSize;
|
|
merc->SetHP(c->GetMercInfo().hp <= 0 ? merc->GetMaxHP() : c->GetMercInfo().hp);
|
|
merc->SetMana(c->GetMercInfo().hp <= 0 ? merc->GetMaxMana() : c->GetMercInfo().mana);
|
|
merc->SetEndurance(c->GetMercInfo().endurance);
|
|
merc->luclinface = c->GetMercInfo().face;
|
|
merc->hairstyle = c->GetMercInfo().luclinHairStyle;
|
|
merc->haircolor = c->GetMercInfo().luclinHairColor;
|
|
merc->eyecolor1 = c->GetMercInfo().luclinEyeColor;
|
|
merc->eyecolor2 = c->GetMercInfo().luclinEyeColor2;
|
|
merc->beardcolor = c->GetMercInfo().luclinBeardColor;
|
|
merc->beard = c->GetMercInfo().luclinBeard;
|
|
merc->drakkin_heritage = c->GetMercInfo().drakkinHeritage;
|
|
merc->drakkin_tattoo = c->GetMercInfo().drakkinTattoo;
|
|
merc->drakkin_details = c->GetMercInfo().drakkinDetails;
|
|
}
|
|
else
|
|
{
|
|
// Give Random Features to newly hired Mercs
|
|
merc->RandomizeFeatures(false, true);
|
|
}
|
|
|
|
if(merc->GetMercenaryID()) {
|
|
database.LoadMercenaryBuffs(merc);
|
|
}
|
|
|
|
merc->LoadMercenarySpells();
|
|
}
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "LoadMerc Successful for %s (%s).", merc->GetName(), c->GetName());
|
|
return merc;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void Merc::UpdateMercInfo(Client *c) {
|
|
snprintf(c->GetMercInfo().merc_name, 64, "%s", name);
|
|
c->GetMercInfo().mercid = GetMercenaryID();
|
|
c->GetMercInfo().IsSuspended = IsSuspended();
|
|
c->GetMercInfo().Gender = GetGender();
|
|
c->GetMercInfo().MercSize = GetSize();
|
|
c->GetMercInfo().hp = GetHP();
|
|
c->GetMercInfo().mana = GetMana();
|
|
c->GetMercInfo().endurance = GetEndurance();
|
|
c->GetMercInfo().face = luclinface;
|
|
c->GetMercInfo().luclinHairStyle = hairstyle;
|
|
c->GetMercInfo().luclinHairColor = haircolor;
|
|
c->GetMercInfo().luclinEyeColor = eyecolor1;
|
|
c->GetMercInfo().luclinEyeColor2 = eyecolor2;
|
|
c->GetMercInfo().luclinBeardColor = beardcolor;
|
|
c->GetMercInfo().luclinBeard = beard;
|
|
c->GetMercInfo().drakkinHeritage = drakkin_heritage;
|
|
c->GetMercInfo().drakkinTattoo = drakkin_tattoo;
|
|
c->GetMercInfo().drakkinDetails = drakkin_details;
|
|
}
|
|
|
|
void Merc::UpdateMercStats(Client *c, bool setmax)
|
|
{
|
|
if (c->GetMercInfo().MercTemplateID > 0) {
|
|
Log(Logs::General, Logs::Mercenaries, "Updating Mercenary Stats for %s (%s).", GetName(),
|
|
c->GetName());
|
|
const NPCType *npc_type = content_db.GetMercenaryType(
|
|
zone->GetMercTemplate(c->GetMercInfo().MercTemplateID)->MercNPCID, GetRace(), c->GetLevel());
|
|
if (npc_type) {
|
|
max_hp = npc_type->max_hp;
|
|
base_hp = npc_type->max_hp;
|
|
max_mana = npc_type->Mana;
|
|
base_mana = npc_type->Mana;
|
|
max_end = npc_type->max_hp; // Hack since Endurance does not exist for NPCType yet
|
|
base_end = npc_type->max_hp; // Hack since Endurance does not exist for NPCType yet
|
|
hp_regen = npc_type->hp_regen;
|
|
hp_regen_per_second = npc_type->hp_regen_per_second;
|
|
mana_regen = npc_type->mana_regen;
|
|
max_dmg = npc_type->max_dmg;
|
|
min_dmg = npc_type->min_dmg;
|
|
|
|
_baseAC = npc_type->AC;
|
|
_baseATK = npc_type->ATK;
|
|
_baseSTR = npc_type->STR;
|
|
_baseSTA = npc_type->STA;
|
|
_baseDEX = npc_type->DEX;
|
|
_baseAGI = npc_type->AGI;
|
|
_baseWIS = npc_type->WIS;
|
|
_baseINT = npc_type->INT;
|
|
_baseCHA = npc_type->CHA;
|
|
_baseATK = npc_type->ATK;
|
|
_baseMR = npc_type->MR;
|
|
_baseFR = npc_type->FR;
|
|
_baseDR = npc_type->DR;
|
|
_basePR = npc_type->PR;
|
|
_baseCR = npc_type->CR;
|
|
_baseCorrup = npc_type->Corrup;
|
|
|
|
uint32 scalepercent = (int)(npc_type->scalerate * RuleI(Mercs, ScaleRate) / 100);
|
|
|
|
ScaleStats(scalepercent, setmax);
|
|
|
|
level = npc_type->level;
|
|
attack_count = npc_type->attack_count;
|
|
attack_delay = npc_type->attack_delay;
|
|
spellscale = npc_type->spellscale;
|
|
healscale = npc_type->healscale;
|
|
|
|
CalcBonuses();
|
|
CalcMaxHP();
|
|
CalcMaxMana();
|
|
CalcMaxEndurance();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Merc::ScaleStats(int scalepercent, bool setmax) {
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "Scaling Mercenary Stats to %d Percent for %s.", scalepercent, GetName());
|
|
|
|
if (scalepercent <= 0)
|
|
return;
|
|
|
|
float scalerate = (float)scalepercent / 100.0f;
|
|
|
|
if ((int64)((float)base_hp * scalerate) > 1)
|
|
{
|
|
max_hp = (int64)((float)base_hp * scalerate);
|
|
base_hp = max_hp;
|
|
if (setmax)
|
|
current_hp = max_hp;
|
|
}
|
|
|
|
if (base_mana)
|
|
{
|
|
max_mana = (int64)((float)base_mana * scalerate);
|
|
base_mana = max_mana;
|
|
if (setmax)
|
|
current_mana = max_mana;
|
|
}
|
|
|
|
if (base_end)
|
|
{
|
|
max_end = (int64)((float)base_end * scalerate);
|
|
base_end = max_end;
|
|
if (setmax)
|
|
cur_end = max_end;
|
|
}
|
|
|
|
if (_baseAC)
|
|
{
|
|
AC = (int)((float)_baseAC * scalerate);
|
|
_baseAC = AC;
|
|
}
|
|
|
|
if (_baseATK)
|
|
{
|
|
ATK = (int)((float)_baseATK * scalerate);
|
|
_baseATK = ATK;
|
|
}
|
|
|
|
if (_baseSTR)
|
|
{
|
|
STR = (int)((float)_baseSTR * scalerate);
|
|
_baseSTR = STR;
|
|
}
|
|
if (_baseSTA)
|
|
{
|
|
STA = (int)((float)_baseSTA * scalerate);
|
|
_baseSTA = STA;
|
|
}
|
|
if (_baseAGI)
|
|
{
|
|
AGI = (int)((float)_baseAGI * scalerate);
|
|
_baseAGI = AGI;
|
|
}
|
|
if (_baseDEX)
|
|
{
|
|
DEX = (int)((float)_baseDEX * scalerate);
|
|
_baseDEX = DEX;
|
|
}
|
|
if (_baseINT)
|
|
{
|
|
INT = (int)((float)_baseINT * scalerate);
|
|
_baseINT = INT;
|
|
}
|
|
if (_baseWIS)
|
|
{
|
|
WIS = (int)((float)_baseWIS * scalerate);
|
|
_baseWIS = WIS;
|
|
}
|
|
if (_baseCHA)
|
|
{
|
|
CHA = (int)((float)_baseCHA * scalerate);
|
|
_baseCHA = CHA;
|
|
}
|
|
|
|
if (_baseMR)
|
|
{
|
|
MR = (int)((float)_baseMR * scalerate);
|
|
_baseMR = MR;
|
|
}
|
|
if (_baseCR)
|
|
{
|
|
CR = (int)((float)_baseCR * scalerate);
|
|
_baseCR = CR;
|
|
}
|
|
if (_baseDR)
|
|
{
|
|
DR = (int)((float)_baseDR * scalerate);
|
|
_baseDR = DR;
|
|
}
|
|
if (_baseFR)
|
|
{
|
|
FR = (int)((float)_baseFR * scalerate);
|
|
_baseFR = FR;
|
|
}
|
|
if (_basePR)
|
|
{
|
|
PR = (int)((float)_basePR * scalerate);
|
|
_basePR = PR;
|
|
}
|
|
if (_baseCorrup)
|
|
{
|
|
Corrup = (int)((float)_baseCorrup * scalerate);
|
|
_baseCorrup = Corrup;
|
|
}
|
|
|
|
if (max_dmg)
|
|
{
|
|
max_dmg = (int)((float)max_dmg * scalerate);
|
|
}
|
|
if (min_dmg)
|
|
{
|
|
min_dmg = (int)((float)min_dmg * scalerate);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void Merc::UpdateMercAppearance() {
|
|
// Copied from Bot Code:
|
|
uint32 itemID = 0;
|
|
uint8 materialFromSlot = EQ::textures::materialInvalid;
|
|
for (int i = EQ::invslot::EQUIPMENT_BEGIN; i <= EQ::invslot::EQUIPMENT_END; ++i) {
|
|
itemID = equipment[i];
|
|
if(itemID != 0) {
|
|
materialFromSlot = EQ::InventoryProfile::CalcMaterialFromSlot(i);
|
|
if (materialFromSlot != EQ::textures::materialInvalid)
|
|
SendWearChange(materialFromSlot);
|
|
}
|
|
}
|
|
|
|
if (UpdateActiveLight())
|
|
SendAppearancePacket(AppearanceType::Light, GetActiveLightType());
|
|
}
|
|
|
|
void Merc::UpdateEquipmentLight()
|
|
{
|
|
m_Light.Type[EQ::lightsource::LightEquipment] = 0;
|
|
m_Light.Level[EQ::lightsource::LightEquipment] = 0;
|
|
|
|
for (int index = EQ::invslot::EQUIPMENT_BEGIN; index <= EQ::invslot::EQUIPMENT_END; ++index) {
|
|
if (index == EQ::invslot::slotAmmo) { continue; }
|
|
|
|
auto item = database.GetItem(equipment[index]);
|
|
if (item == nullptr) { continue; }
|
|
|
|
if (EQ::lightsource::IsLevelGreater(item->Light, m_Light.Type[EQ::lightsource::LightEquipment])) {
|
|
m_Light.Type[EQ::lightsource::LightEquipment] = item->Light;
|
|
m_Light.Level[EQ::lightsource::LightEquipment] = EQ::lightsource::TypeToLevel(m_Light.Type[EQ::lightsource::LightEquipment]);
|
|
}
|
|
}
|
|
|
|
uint8 general_light_type = 0;
|
|
for (auto iter = m_loot_items.begin(); iter != m_loot_items.end(); ++iter) {
|
|
auto item = database.GetItem((*iter)->item_id);
|
|
if (item == nullptr) { continue; }
|
|
|
|
if (!item->IsClassCommon()) { continue; }
|
|
if (item->Light < 9 || item->Light > 13) { continue; }
|
|
|
|
if (EQ::lightsource::TypeToLevel(item->Light))
|
|
general_light_type = item->Light;
|
|
}
|
|
|
|
if (EQ::lightsource::IsLevelGreater(general_light_type, m_Light.Type[EQ::lightsource::LightEquipment]))
|
|
m_Light.Type[EQ::lightsource::LightEquipment] = general_light_type;
|
|
|
|
m_Light.Level[EQ::lightsource::LightEquipment] = EQ::lightsource::TypeToLevel(m_Light.Type[EQ::lightsource::LightEquipment]);
|
|
}
|
|
|
|
void Merc::AddItem(uint8 slot, uint32 item_id) {
|
|
equipment[slot] = item_id;
|
|
UpdateEquipmentLight();
|
|
}
|
|
|
|
bool Merc::Spawn(Client *owner) {
|
|
|
|
if(!owner)
|
|
return false;
|
|
|
|
MercTemplate* merc_template = zone->GetMercTemplate(GetMercenaryTemplateID());
|
|
|
|
if(!merc_template)
|
|
return false;
|
|
|
|
entity_list.AddMerc(this, true, true);
|
|
|
|
SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0);
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "Spawn Mercenary %s.", GetName());
|
|
|
|
//UpdateMercAppearance();
|
|
|
|
return true;
|
|
}
|
|
|
|
void Client::SendMercResponsePackets(uint32 ResponseType)
|
|
{
|
|
switch (ResponseType)
|
|
{
|
|
case 0: // Mercenary Spawned Successfully?
|
|
SendMercMerchantResponsePacket(0);
|
|
break;
|
|
case 1: //You do not have enough funds to make that purchase!
|
|
SendMercMerchantResponsePacket(1);
|
|
break;
|
|
case 2: //Mercenary does not exist!
|
|
SendMercMerchantResponsePacket(2);
|
|
break;
|
|
case 3: //Mercenary failed to spawn!
|
|
SendMercMerchantResponsePacket(3);
|
|
break;
|
|
case 4: //Mercenaries are not allowed in raids!
|
|
SendMercMerchantResponsePacket(4);
|
|
break;
|
|
case 5: //You already have a pending mercenary purchase!
|
|
SendMercMerchantResponsePacket(5);
|
|
break;
|
|
case 6: //You have the maximum number of mercenaries. You must dismiss one before purchasing a new one!
|
|
SendMercMerchantResponsePacket(6);
|
|
break;
|
|
case 7: //You must dismiss your suspended mercenary before purchasing a new one!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(7);
|
|
else
|
|
//You have the maximum number of mercenaries. You must dismiss one before purchasing a new one!
|
|
SendMercMerchantResponsePacket(6);
|
|
break;
|
|
case 8: //You can not purchase a mercenary because your group is full!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(8);
|
|
else
|
|
SendMercMerchantResponsePacket(7);
|
|
break;
|
|
case 9: //You can not purchase a mercenary because you are in combat!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
//Mercenary failed to spawn!
|
|
SendMercMerchantResponsePacket(3);
|
|
else
|
|
SendMercMerchantResponsePacket(8);
|
|
break;
|
|
case 10: //You have recently dismissed a mercenary and must wait a few more seconds before you can purchase a new one!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
//Mercenary failed to spawn!
|
|
SendMercMerchantResponsePacket(3);
|
|
else
|
|
SendMercMerchantResponsePacket(9);
|
|
break;
|
|
case 11: //An error occurred created your mercenary!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(9);
|
|
else
|
|
SendMercMerchantResponsePacket(10);
|
|
break;
|
|
case 12: //Upkeep Charge Message
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(10);
|
|
else
|
|
SendMercMerchantResponsePacket(11);
|
|
break;
|
|
case 13: // ???
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(11);
|
|
else
|
|
SendMercMerchantResponsePacket(12);
|
|
break;
|
|
case 14: //You ran out of funds to pay for your mercenary!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(12);
|
|
else
|
|
SendMercMerchantResponsePacket(13);
|
|
break;
|
|
case 15: // ???
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(13);
|
|
else
|
|
SendMercMerchantResponsePacket(14);
|
|
break;
|
|
case 16: //Your mercenary is about to be suspended due to insufficient funds!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(14);
|
|
else
|
|
SendMercMerchantResponsePacket(15);
|
|
break;
|
|
case 17: //There is no mercenary liaison nearby!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(15);
|
|
else
|
|
SendMercMerchantResponsePacket(16);
|
|
break;
|
|
case 18: //You are too far from the liaison!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(16);
|
|
else
|
|
SendMercMerchantResponsePacket(17);
|
|
break;
|
|
case 19: //You do not meet the requirements for that mercenary!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
SendMercMerchantResponsePacket(17);
|
|
else
|
|
SendMercMerchantResponsePacket(18);
|
|
break;
|
|
case 20: //You are unable to interact with the liaison!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
//You are too far from the liaison!
|
|
SendMercMerchantResponsePacket(16);
|
|
else
|
|
SendMercMerchantResponsePacket(19);
|
|
break;
|
|
case 21: //You do not have a high enough membership level to purchase this mercenary!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
//You do not meet the requirements for that mercenary!
|
|
SendMercMerchantResponsePacket(17);
|
|
else
|
|
SendMercMerchantResponsePacket(20);
|
|
break;
|
|
case 22: //Your purchase has failed because this mercenary requires a Gold membership!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
//You do not meet the requirements for that mercenary!
|
|
SendMercMerchantResponsePacket(17);
|
|
else
|
|
SendMercMerchantResponsePacket(21);
|
|
break;
|
|
case 23: //Your purchase has failed because this mercenary requires at least a Silver membership!
|
|
if (ClientVersion() < EQ::versions::ClientVersion::RoF)
|
|
//You do not meet the requirements for that mercenary!
|
|
SendMercMerchantResponsePacket(17);
|
|
else
|
|
SendMercMerchantResponsePacket(22);
|
|
break;
|
|
default: //Mercenary failed to spawn!
|
|
SendMercMerchantResponsePacket(3);
|
|
break;
|
|
}
|
|
Log(Logs::General, Logs::Mercenaries, "SendMercResponsePackets %i for %s.", ResponseType, GetName());
|
|
|
|
}
|
|
|
|
void Client::UpdateMercTimer()
|
|
{
|
|
Merc *merc = GetMerc();
|
|
|
|
if(merc && !merc->IsSuspended())
|
|
{
|
|
if(GetMercTimer()->Check())
|
|
{
|
|
uint32 upkeep = merc->CalcUpkeepCost(merc->GetMercenaryTemplateID(), GetLevel());
|
|
|
|
if(CheckCanRetainMerc(upkeep))
|
|
{
|
|
if(RuleB(Mercs, ChargeMercUpkeepCost))
|
|
{
|
|
TakeMoneyFromPP((upkeep * 100), true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
merc->Suspend();
|
|
return;
|
|
}
|
|
|
|
// Reset the upkeep timer
|
|
GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS);
|
|
SendMercTimer(merc);
|
|
GetMercTimer()->Start(RuleI(Mercs, UpkeepIntervalMS));
|
|
GetMercTimer()->SetTimer(GetMercInfo().MercTimerRemaining);
|
|
|
|
// Send upkeep charge message
|
|
SendMercResponsePackets(12);
|
|
|
|
// Warn that mercenary is about to be suspended due to insufficient funds (on next upkeep)
|
|
if (RuleB(Mercs, ChargeMercUpkeepCost) && upkeep > 0 && !HasMoney(upkeep * 100))
|
|
{
|
|
SendMercResponsePackets(16);
|
|
}
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "UpdateMercTimer Complete for %s.", GetName());
|
|
|
|
// Normal upkeep charge message
|
|
//Message(Chat::LightGray, "You have been charged a mercenary upkeep cost of %i plat, and %i gold and your mercenary upkeep cost timer has been reset to 15 minutes.", upkeep_plat, upkeep_gold, (int)(RuleI(Mercs, UpkeepIntervalMS) / 1000 / 60));
|
|
|
|
// Message below given when too low level to be charged
|
|
//Message(Chat::LightGray, "Your mercenary waived an upkeep cost of %i plat, and %i gold or %i %s and your mercenary upkeep cost timer has been reset to %i minutes", upkeep_plat, upkeep_gold, 1, "Bayle Marks", (int)(RuleI(Mercs, UpkeepIntervalMS) / 1000 / 60));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Client::CheckCanHireMerc(Mob* merchant, uint32 template_id) {
|
|
|
|
if (!CheckCanSpawnMerc(template_id))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
MercTemplate* mercTemplate = zone->GetMercTemplate(template_id);
|
|
|
|
//check for suspended merc
|
|
if(GetMercInfo().mercid != 0 && GetMercInfo().IsSuspended) {
|
|
SendMercResponsePackets(6);
|
|
return false;
|
|
}
|
|
|
|
// Check if max number of mercs is already reached
|
|
if(GetNumberOfMercenaries() >= MAXMERCS) {
|
|
SendMercResponsePackets(6);
|
|
return false;
|
|
}
|
|
|
|
//check for valid merchant
|
|
if(!merchant) {
|
|
SendMercResponsePackets(17);
|
|
return false;
|
|
}
|
|
|
|
//check for merchant too far away
|
|
if(DistanceSquared(m_Position, merchant->GetPosition()) > USE_NPC_RANGE2) {
|
|
SendMercResponsePackets(18);
|
|
return false;
|
|
}
|
|
|
|
//check for sufficient funds and remove them last
|
|
if(RuleB(Mercs, ChargeMercPurchaseCost)) {
|
|
uint32 cost = Merc::CalcPurchaseCost(template_id, GetLevel()) * 100; // Cost is in gold
|
|
if(cost > 0 && !HasMoney(cost)) {
|
|
SendMercResponsePackets(1);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "CheckCanHireMerc True for %s.", GetName());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Client::CheckCanRetainMerc(uint32 upkeep) {
|
|
Merc* merc = GetMerc();
|
|
|
|
//check for sufficient funds
|
|
if(RuleB(Mercs, ChargeMercPurchaseCost)) {
|
|
if(merc) {
|
|
if(upkeep > 0 && !HasMoney(upkeep * 100)) {
|
|
SendMercResponsePackets(14);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Client::CheckCanSpawnMerc(uint32 template_id) {
|
|
|
|
// Check if mercs are enabled globally
|
|
if(!RuleB(Mercs, AllowMercs))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if zone allows mercs
|
|
if(!zone->AllowMercs())
|
|
{
|
|
SendMercResponsePackets(3);
|
|
return false;
|
|
}
|
|
|
|
MercTemplate* mercTemplate = zone->GetMercTemplate(template_id);
|
|
|
|
// Invalid merc data
|
|
if(!mercTemplate)
|
|
{
|
|
SendMercResponsePackets(11);
|
|
return false;
|
|
}
|
|
|
|
// Check client version
|
|
if(static_cast<unsigned int>(ClientVersion()) < mercTemplate->ClientVersion)
|
|
{
|
|
SendMercResponsePackets(3);
|
|
return false;
|
|
}
|
|
|
|
// Check for raid
|
|
if(HasRaid())
|
|
{
|
|
SendMercResponsePackets(4);
|
|
return false;
|
|
}
|
|
|
|
// Check group size
|
|
if(GetGroup() && GetGroup()->GroupCount() >= MAX_GROUP_MEMBERS) // database.GroupCount(GetGroup()->GetID())
|
|
{
|
|
SendMercResponsePackets(8);
|
|
return false;
|
|
}
|
|
|
|
// Check in combat
|
|
if(GetAggroCount() > 0)
|
|
{
|
|
SendMercResponsePackets(9);
|
|
return false;
|
|
}
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "CheckCanSpawnMerc True for %s.", GetName());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Client::CheckCanUnsuspendMerc() {
|
|
|
|
if (!CheckCanSpawnMerc(GetMercInfo().MercTemplateID))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
MercTemplate* mercTemplate = zone->GetMercTemplate(GetMercInfo().MercTemplateID);
|
|
|
|
if(!GetPTimers().Expired(&database, pTimerMercSuspend, false))
|
|
{
|
|
SendMercResponsePackets(10);
|
|
//TODO: find this packet response and tell them properly.
|
|
Message(0, "You must wait %i seconds before unsuspending your mercenary.", GetPTimers().GetRemainingTime(pTimerMercSuspend));
|
|
return false;
|
|
}
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "CheckCanUnsuspendMerc True for %s.", GetName());
|
|
|
|
return true;
|
|
}
|
|
|
|
void Client::CheckMercSuspendTimer() {
|
|
|
|
if(GetMercInfo().SuspendedTime != 0)
|
|
{
|
|
//if(time(nullptr) >= GetMercInfo().SuspendedTime)
|
|
if (p_timers.Expired(&database, pTimerMercSuspend, false))
|
|
{
|
|
GetMercInfo().SuspendedTime = 0;
|
|
SendMercResponsePackets(0);
|
|
SendMercSuspendResponsePacket(GetMercInfo().SuspendedTime);
|
|
Log(Logs::General, Logs::Mercenaries, "CheckMercSuspendTimer Ready for %s.", GetName());
|
|
}
|
|
}
|
|
}
|
|
|
|
void Client::SuspendMercCommand() {
|
|
|
|
if(GetMercInfo().MercTemplateID != 0)
|
|
{
|
|
if(GetMercInfo().IsSuspended)
|
|
{
|
|
if(!CheckCanUnsuspendMerc())
|
|
{
|
|
Log(Logs::General, Logs::Mercenaries, "SuspendMercCommand Unable to Unsuspend Merc for %s.", GetName());
|
|
|
|
return;
|
|
}
|
|
|
|
// Get merc, assign it to client & spawn
|
|
Merc* merc = Merc::LoadMercenary(this, &zone->merc_templates[GetMercInfo().MercTemplateID], 0, true);
|
|
if(merc)
|
|
{
|
|
SpawnMerc(merc, false);
|
|
Log(Logs::General, Logs::Mercenaries, "SuspendMercCommand Successful Unsuspend for %s.", GetName());
|
|
}
|
|
else
|
|
{
|
|
//merc failed to spawn
|
|
SendMercResponsePackets(3);
|
|
Log(Logs::General, Logs::Mercenaries, "SuspendMercCommand Failed to Spawn Merc for %s.", GetName());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Merc* CurrentMerc = GetMerc();
|
|
|
|
|
|
if (!RuleB(Mercs, AllowMercSuspendInCombat))
|
|
{
|
|
if (!CheckCanSpawnMerc(GetMercInfo().MercTemplateID))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(CurrentMerc && GetMercenaryID())
|
|
{
|
|
CurrentMerc->Suspend();
|
|
Log(Logs::General, Logs::Mercenaries, "SuspendMercCommand Successful Suspend for %s.", GetName());
|
|
}
|
|
else
|
|
{
|
|
// Reset Merc Suspend State
|
|
GetMercInfo().IsSuspended = true;
|
|
//GetMercInfo().SuspendedTime = time(nullptr) + RuleI(Mercs, SuspendIntervalS);
|
|
//GetMercInfo().MercTimerRemaining = GetMercTimer()->GetRemainingTime();
|
|
//GetMercInfo().Stance = GetStance();
|
|
GetMercTimer()->Disable();
|
|
SendMercSuspendResponsePacket(GetMercInfo().SuspendedTime);
|
|
SendMercTimer(nullptr);
|
|
Log(Logs::General, Logs::Mercenaries, "SuspendMercCommand Failed to Get Merc to Suspend. Resetting Suspend State for %s.", GetName());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SpawnMercOnZone();
|
|
Log(Logs::General, Logs::Mercenaries, "SuspendMercCommand Request Failed to Load Merc for %s. Trying SpawnMercOnZone.", GetName());
|
|
}
|
|
}
|
|
|
|
|
|
// Handles all client zone change event
|
|
void Merc::ProcessClientZoneChange(Client* mercOwner) {
|
|
|
|
if(mercOwner)
|
|
{
|
|
Zone();
|
|
}
|
|
}
|
|
|
|
void Client::SpawnMercOnZone() {
|
|
|
|
if(!RuleB(Mercs, AllowMercs))
|
|
return;
|
|
|
|
if (GetMerc())
|
|
return;
|
|
|
|
if(database.LoadMercenaryInfo(this))
|
|
{
|
|
if(!GetMercInfo().IsSuspended)
|
|
{
|
|
GetMercInfo().SuspendedTime = 0;
|
|
// Get merc, assign it to client & spawn
|
|
Merc* merc = Merc::LoadMercenary(this, &zone->merc_templates[GetMercInfo().MercTemplateID], 0, true);
|
|
if(merc)
|
|
{
|
|
SpawnMerc(merc, false);
|
|
}
|
|
Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Normal Merc for %s.", GetName());
|
|
}
|
|
else
|
|
{
|
|
int32 TimeDiff = GetMercInfo().SuspendedTime - time(nullptr);
|
|
if (TimeDiff > 0)
|
|
{
|
|
if (!GetPTimers().Enabled(pTimerMercSuspend))
|
|
{
|
|
// Start the timer to send the packet that refreshes the Unsuspend Button
|
|
GetPTimers().Start(pTimerMercSuspend, TimeDiff);
|
|
}
|
|
}
|
|
// Send Mercenary Status/Timer packet
|
|
SendMercTimer(GetMerc());
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Suspended Merc for %s.", GetName());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No Merc Hired
|
|
// RoF+ displays a message from the following packet, which seems useless
|
|
//SendClearMercInfo();
|
|
Log(Logs::General, Logs::Mercenaries, "SpawnMercOnZone Failed to load Merc Info from the Database for %s.", GetName());
|
|
}
|
|
}
|
|
|
|
void Client::SendMercTimer(Merc* merc) {
|
|
|
|
if (GetMercInfo().mercid == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!merc)
|
|
{
|
|
SendMercTimerPacket(NO_MERC_ID, MERC_STATE_SUSPENDED, GetMercInfo().SuspendedTime, GetMercInfo().MercTimerRemaining, RuleI(Mercs, SuspendIntervalMS));
|
|
Log(Logs::General, Logs::Mercenaries, "SendMercTimer No Merc for %s.", GetName());
|
|
}
|
|
else if (merc->IsSuspended())
|
|
{
|
|
SendMercTimerPacket(NO_MERC_ID, MERC_STATE_SUSPENDED, GetMercInfo().SuspendedTime, GetMercInfo().MercTimerRemaining, RuleI(Mercs, SuspendIntervalMS));
|
|
Log(Logs::General, Logs::Mercenaries, "SendMercTimer Suspended Merc for %s.", GetName());
|
|
}
|
|
else
|
|
{
|
|
SendMercTimerPacket(merc->GetID(), MERC_STATE_NORMAL, NOT_SUSPENDED_TIME, GetMercInfo().MercTimerRemaining, RuleI(Mercs, SuspendIntervalMS));
|
|
Log(Logs::General, Logs::Mercenaries, "SendMercTimer Normal Merc for %s.", GetName());
|
|
}
|
|
|
|
}
|
|
|
|
void Client::SpawnMerc(Merc* merc, bool setMaxStats) {
|
|
|
|
if (!merc || !CheckCanSpawnMerc(merc->GetMercenaryTemplateID()))
|
|
{
|
|
if (merc)
|
|
{
|
|
merc->Suspend();
|
|
}
|
|
return;
|
|
}
|
|
|
|
merc->Spawn(this);
|
|
merc->SetSuspended(false);
|
|
SetMerc(merc);
|
|
merc->Unsuspend(setMaxStats);
|
|
merc->SetStance(GetMercInfo().Stance);
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "SpawnMerc Success for %s.", GetName());
|
|
}
|
|
|
|
bool Merc::Suspend() {
|
|
|
|
Client* mercOwner = GetMercenaryOwner();
|
|
|
|
if(!mercOwner)
|
|
return false;
|
|
|
|
SetSuspended(true);
|
|
|
|
mercOwner->GetMercInfo().IsSuspended = true;
|
|
mercOwner->GetMercInfo().SuspendedTime = time(nullptr) + RuleI(Mercs, SuspendIntervalS);
|
|
mercOwner->GetMercInfo().MercTimerRemaining = mercOwner->GetMercTimer()->GetRemainingTime();
|
|
mercOwner->GetMercInfo().Stance = GetStance();
|
|
Save();
|
|
mercOwner->GetMercTimer()->Disable();
|
|
mercOwner->SendMercSuspendResponsePacket(mercOwner->GetMercInfo().SuspendedTime);
|
|
mercOwner->SendMercTimer(this);
|
|
|
|
Depop();
|
|
|
|
// Start the timer to send the packet that refreshes the Unsuspend Button
|
|
mercOwner->GetPTimers().Start(pTimerMercSuspend, RuleI(Mercs, SuspendIntervalS));
|
|
|
|
Log(Logs::General, Logs::Mercenaries, "Suspend Complete for %s.", mercOwner->GetName());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Client::MercOnlyOrNoGroup() {
|
|
|
|
if (!GetGroup())
|
|
{
|
|
return true;
|
|
}
|
|
if (GetMerc())
|
|
{
|
|
if (GetMerc()->GetGroup() == GetGroup())
|
|
{
|
|
if (GetGroup()->GroupCount() < 3)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Merc::Unsuspend(bool setMaxStats) {
|
|
|
|
Client* mercOwner = nullptr;
|
|
|
|
if(GetMercenaryOwner()) {
|
|
mercOwner = GetMercenaryOwner();
|
|
}
|
|
|
|
if(!mercOwner)
|
|
return false;
|
|
|
|
if(GetID())
|
|
{
|
|
// Set time remaining to max on unsuspend - there is a charge for unsuspending as well
|
|
SetSuspended(false);
|
|
|
|
mercOwner->GetMercInfo().mercid = GetMercenaryID();
|
|
mercOwner->GetMercInfo().IsSuspended = false;
|
|
|
|
mercOwner->SendMercenaryUnsuspendPacket(0);
|
|
mercOwner->SendMercenaryUnknownPacket(1);
|
|
mercOwner->GetMercInfo().SuspendedTime = 0;
|
|
// Reset the upkeep timer
|
|
mercOwner->GetMercInfo().MercTimerRemaining = RuleI(Mercs, UpkeepIntervalMS);
|
|
mercOwner->GetMercTimer()->Start(RuleI(Mercs, UpkeepIntervalMS));
|
|
//mercOwner->GetMercTimer()->SetTimer(mercOwner->GetMercInfo().MercTimerRemaining);
|
|
mercOwner->SendMercTimer(this);
|
|
if(!mercOwner->GetPTimers().Expired(&database, pTimerMercSuspend, false))
|
|
mercOwner->GetPTimers().Clear(&database, pTimerMercSuspend);
|
|
|
|
if (MercJoinClientGroup())
|
|
{
|
|
if(setMaxStats)
|
|
{
|
|
RestoreHealth();
|
|
RestoreMana();
|
|
RestoreEndurance();
|
|
}
|
|
|
|
//check for sufficient funds and remove them last
|
|
if(RuleB(Mercs, ChargeMercUpkeepCost))
|
|
{
|
|
uint32 cost = CalcUpkeepCost(GetMercenaryTemplateID(), GetLevel()) * 100; // Cost is in gold
|
|
if(cost > 0 && !mercOwner->HasMoney(cost))
|
|
{
|
|
mercOwner->SendMercResponsePackets(1);
|
|
Suspend();
|
|
return false;
|
|
}
|
|
}
|
|
Save();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Client::DismissMerc(uint32 MercID) {
|
|
|
|
bool Dismissed = true;
|
|
if (!database.DeleteMercenary(MercID))
|
|
{
|
|
Log(Logs::General, Logs::Mercenaries, "Dismiss Failed Database Query for MercID: %i, Client: %s.", MercID, GetName());
|
|
Dismissed = false;
|
|
}
|
|
else
|
|
{
|
|
Log(Logs::General, Logs::Mercenaries, "Dismiss Successful for %s.", GetName());
|
|
}
|
|
|
|
if (GetMerc())
|
|
{
|
|
GetMerc()->Depop();
|
|
}
|
|
|
|
SendClearMercInfo();
|
|
SetMerc(nullptr);
|
|
|
|
return Dismissed;
|
|
}
|
|
|
|
void Merc::Zone() {
|
|
Save();
|
|
Depop();
|
|
}
|
|
|
|
void Merc::Depop() {
|
|
|
|
WipeHateList();
|
|
|
|
if(IsCasting())
|
|
{
|
|
InterruptSpell();
|
|
}
|
|
|
|
entity_list.RemoveFromHateLists(this);
|
|
|
|
if(GetGroup())
|
|
{
|
|
RemoveMercFromGroup(this, GetGroup());
|
|
}
|
|
|
|
entity_list.RemoveMerc(GetID());
|
|
|
|
if(HasPet())
|
|
{
|
|
GetPet()->Depop();
|
|
}
|
|
|
|
p_depop = true;
|
|
|
|
NPC::Depop(false);
|
|
}
|
|
|
|
bool Merc::RemoveMercFromGroup(Merc* merc, Group* group) {
|
|
|
|
bool Result = false;
|
|
|
|
if(merc && group)
|
|
{
|
|
uint32 groupID = group->GetID();
|
|
if(merc->HasGroup())
|
|
{
|
|
if(!group->IsLeader(merc))
|
|
{
|
|
merc->SetFollowID(0);
|
|
|
|
if (group->GroupCount() <= 2 && merc->GetGroup() == group && is_zone_loaded)
|
|
{
|
|
group->DisbandGroup();
|
|
}
|
|
else if(group->DelMember(merc, true))
|
|
{
|
|
if(merc->GetMercenaryCharacterID() != 0)
|
|
{
|
|
Group::RemoveFromGroup(merc);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// A merc is group leader - Disband and re-group each member with their mercs
|
|
for(int i = 0; i < MAX_GROUP_MEMBERS; i++)
|
|
{
|
|
if(!group->members[i])
|
|
continue;
|
|
|
|
if(!group->members[i]->IsClient())
|
|
continue;
|
|
|
|
Client *groupMember = group->members[i]->CastToClient();
|
|
groupMember->LeaveGroup();
|
|
if (groupMember->GetMerc())
|
|
{
|
|
groupMember->GetMerc()->MercJoinClientGroup();
|
|
}
|
|
}
|
|
// Group should be removed by now, but just in case:
|
|
Group *oldGroup = entity_list.GetGroupByID(groupID);
|
|
if (oldGroup != nullptr)
|
|
{
|
|
oldGroup->DisbandGroup();
|
|
}
|
|
}
|
|
|
|
Result = true;
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
bool Merc::MercJoinClientGroup() {
|
|
|
|
Client* mercOwner = nullptr;
|
|
|
|
if(GetMercenaryOwner())
|
|
{
|
|
mercOwner = GetMercenaryOwner();
|
|
}
|
|
|
|
if(!mercOwner)
|
|
{
|
|
Suspend();
|
|
return false;
|
|
}
|
|
|
|
if(GetID())
|
|
{
|
|
if (HasGroup())
|
|
{
|
|
RemoveMercFromGroup(this, GetGroup());
|
|
}
|
|
|
|
Group* g = entity_list.GetGroupByClient(mercOwner);
|
|
|
|
//nobody from our group is here... start a new group
|
|
if(!g)
|
|
{
|
|
g = new Group(mercOwner);
|
|
|
|
if(!g)
|
|
{
|
|
delete g;
|
|
g = nullptr;
|
|
return false;
|
|
}
|
|
|
|
entity_list.AddGroup(g);
|
|
|
|
if(g->GetID() == 0)
|
|
{
|
|
|
|
delete g;
|
|
g = nullptr;
|
|
return false;
|
|
}
|
|
|
|
if (AddMercToGroup(this, g))
|
|
{
|
|
g->AddToGroup(mercOwner);
|
|
database.SetGroupLeaderName(g->GetID(), mercOwner->GetName());
|
|
database.RefreshGroupFromDB(mercOwner);
|
|
g->SaveGroupLeaderAA();
|
|
Log(Logs::General, Logs::Mercenaries, "Mercenary joined new group: %s (%s).", GetName(), mercOwner->GetName());
|
|
}
|
|
else
|
|
{
|
|
g->DisbandGroup();
|
|
Suspend();
|
|
Log(Logs::General, Logs::Mercenaries, "Mercenary disbanded new group: %s (%s).", GetName(), mercOwner->GetName());
|
|
}
|
|
|
|
}
|
|
else if (AddMercToGroup(this, mercOwner->GetGroup()))
|
|
{
|
|
// Group already exists
|
|
database.RefreshGroupFromDB(mercOwner);
|
|
// Update members that are out of zone
|
|
GetGroup()->SendGroupJoinOOZ(this);
|
|
Log(Logs::General, Logs::Mercenaries, "Mercenary %s joined existing group with %s.", GetName(), mercOwner->GetName());
|
|
}
|
|
else
|
|
{
|
|
Suspend();
|
|
Log(Logs::General, Logs::Mercenaries, "Mercenary failed to join the group - Suspending %s for (%s).", GetName(), mercOwner->GetName());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Merc::AddMercToGroup(Merc* merc, Group* group) {
|
|
bool Result = false;
|
|
|
|
if(merc && group) {
|
|
// Remove merc from current group if it's not the destination group
|
|
if(merc->HasGroup())
|
|
{
|
|
if(merc->GetGroup() == group && merc->GetMercenaryOwner())
|
|
{
|
|
// Merc is already in the destination group
|
|
merc->SetFollowID(merc->GetMercenaryOwner()->GetID());
|
|
return true;
|
|
}
|
|
merc->RemoveMercFromGroup(merc, merc->GetGroup());
|
|
}
|
|
//Try and add the member, followed by checking if the merc owner exists.
|
|
if(group->AddMember(merc) && merc->GetMercenaryOwner())
|
|
{
|
|
merc->SetFollowID(merc->GetMercenaryOwner()->GetID());
|
|
Result = true;
|
|
}
|
|
else
|
|
{
|
|
//Suspend it if the member is not added or the merc's owner is not valid.
|
|
merc->Suspend();
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
void Client::InitializeMercInfo() {
|
|
|
|
for(int i=0; i<MAXMERCS; i++)
|
|
{
|
|
m_mercinfo[i] = MercInfo();
|
|
}
|
|
|
|
}
|
|
|
|
Merc* Client::GetMerc() {
|
|
|
|
if(GetMercenaryID() == 0)
|
|
{
|
|
Log(Logs::Detail, Logs::Mercenaries, "GetMerc - GetMercenaryID: 0 for %s.", GetName());
|
|
return (nullptr);
|
|
}
|
|
|
|
Merc* tmp = entity_list.GetMercByID(GetMercenaryID());
|
|
if(tmp == nullptr)
|
|
{
|
|
SetMercID(0);
|
|
Log(Logs::Detail, Logs::Mercenaries, "GetMerc No Merc for %s.", GetName());
|
|
return (nullptr);
|
|
}
|
|
|
|
if(tmp->GetOwnerID() != GetID())
|
|
{
|
|
SetMercID(0);
|
|
Log(Logs::Detail, Logs::Mercenaries, "GetMerc Owner Mismatch - OwnerID: %d, ClientID: %d, Client: %s.", tmp->GetOwnerID(), GetID(), GetName());
|
|
return (nullptr);
|
|
}
|
|
|
|
return (tmp);
|
|
}
|
|
|
|
uint8 Client::GetNumberOfMercenaries()
|
|
{
|
|
uint8 count = 0;
|
|
|
|
for (int slot_id = 0; slot_id < MAXMERCS; slot_id++) {
|
|
if (m_mercinfo[slot_id].mercid != 0) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void Merc::SetMercData( uint32 template_id ) {
|
|
|
|
MercTemplate* merc_template = zone->GetMercTemplate(template_id);
|
|
SetMercTemplateID( merc_template->MercTemplateID );
|
|
SetMercType( merc_template->MercType );
|
|
SetMercSubType( merc_template->MercSubType );
|
|
SetProficiencyID( merc_template->ProficiencyID );
|
|
SetTierID( merc_template->TierID );
|
|
SetCostFormula( merc_template->CostFormula );
|
|
SetMercNameType( merc_template->MercNameType );
|
|
|
|
}
|
|
|
|
MercTemplate* Zone::GetMercTemplate( uint32 template_id ) {
|
|
return &merc_templates[template_id];
|
|
}
|
|
|
|
void Client::SetMerc(Merc* newmerc) {
|
|
|
|
Merc* oldmerc = GetMerc();
|
|
if (oldmerc)
|
|
{
|
|
oldmerc->SetOwnerID(0);
|
|
}
|
|
|
|
if (!newmerc)
|
|
{
|
|
SetMercID(0);
|
|
GetMercInfo().mercid = 0;
|
|
GetMercInfo().MercTemplateID = 0;
|
|
GetMercInfo().myTemplate = nullptr;
|
|
GetMercInfo().IsSuspended = false;
|
|
GetMercInfo().SuspendedTime = 0;
|
|
GetMercInfo().Gender = Gender::Male;
|
|
GetMercInfo().State = 0;
|
|
memset(GetMercInfo().merc_name, 0, 64);
|
|
Log(Logs::General, Logs::Mercenaries, "SetMerc No Merc for %s.", GetName());
|
|
}
|
|
else
|
|
{
|
|
SetMercID(newmerc->GetID());
|
|
//Client* oldowner = entity_list.GetClientByID(newmerc->GetOwnerID());
|
|
newmerc->SetOwnerID(GetID());
|
|
newmerc->SetMercCharacterID(CharacterID());
|
|
newmerc->SetClientVersion((uint8)ClientVersion());
|
|
GetMercInfo().mercid = newmerc->GetMercenaryID();
|
|
GetMercInfo().MercTemplateID = newmerc->GetMercenaryTemplateID();
|
|
GetMercInfo().myTemplate = zone->GetMercTemplate(GetMercInfo().MercTemplateID);
|
|
GetMercInfo().IsSuspended = newmerc->IsSuspended();
|
|
GetMercInfo().SuspendedTime = 0;
|
|
GetMercInfo().Gender = newmerc->GetGender();
|
|
GetMercInfo().State = newmerc->IsSuspended() ? MERC_STATE_SUSPENDED : MERC_STATE_NORMAL;
|
|
snprintf(GetMercInfo().merc_name, 64, "%s", newmerc->GetName());
|
|
Log(Logs::General, Logs::Mercenaries, "SetMerc New Merc for %s.", GetName());
|
|
}
|
|
}
|
|
|
|
void Client::UpdateMercLevel() {
|
|
Merc* merc = GetMerc();
|
|
if (merc)
|
|
{
|
|
merc->UpdateMercStats(this, false);
|
|
merc->SendAppearancePacket(AppearanceType::WhoLevel, GetLevel(), true, true);
|
|
}
|
|
}
|
|
|
|
void Client::SendMercMerchantResponsePacket(int32 response_type) {
|
|
// This response packet brings up the Mercenary Manager window
|
|
if (ClientVersion() >= EQ::versions::ClientVersion::SoD)
|
|
{
|
|
auto outapp = new EQApplicationPacket(OP_MercenaryHire, sizeof(MercenaryMerchantResponse_Struct));
|
|
MercenaryMerchantResponse_Struct* mmr = (MercenaryMerchantResponse_Struct*)outapp->pBuffer;
|
|
mmr->ResponseType = response_type; // send specified response type
|
|
FastQueuePacket(&outapp);
|
|
Log(Logs::Detail, Logs::Mercenaries, "Sent SendMercMerchantResponsePacket ResponseType: %i, Client: %s.", response_type, GetName());
|
|
}
|
|
}
|
|
|
|
void Client::SendMercenaryUnknownPacket(uint8 type) {
|
|
|
|
auto outapp = new EQApplicationPacket(OP_MercenaryUnknown1, 1);
|
|
outapp->WriteUInt8(type);
|
|
FastQueuePacket(&outapp);
|
|
Log(Logs::Detail, Logs::Mercenaries, "Sent SendMercenaryUnknownPacket Type: %i, Client: %s.", type, GetName());
|
|
|
|
}
|
|
|
|
void Client::SendMercenaryUnsuspendPacket(uint8 type) {
|
|
|
|
auto outapp = new EQApplicationPacket(OP_MercenaryUnsuspendResponse, 1);
|
|
outapp->WriteUInt8(type);
|
|
FastQueuePacket(&outapp);
|
|
Log(Logs::Detail, Logs::Mercenaries, "Sent SendMercenaryUnsuspendPacket Type: %i, Client: %s.", type, GetName());
|
|
|
|
}
|
|
|
|
void Client::SendMercSuspendResponsePacket(uint32 suspended_time) {
|
|
|
|
auto outapp = new EQApplicationPacket(OP_MercenarySuspendResponse, sizeof(SuspendMercenaryResponse_Struct));
|
|
SuspendMercenaryResponse_Struct* smr = (SuspendMercenaryResponse_Struct*)outapp->pBuffer;
|
|
smr->SuspendTime = suspended_time; // Seen 0 (not suspended) or c9 c2 64 4f (suspended on Sat Mar 17 11:58:49 2012) - Unix Timestamp
|
|
FastQueuePacket(&outapp);
|
|
Log(Logs::Detail, Logs::Mercenaries, "Sent SendMercSuspendResponsePacket Time: %i, Client: %s.", suspended_time, GetName());
|
|
|
|
}
|
|
|
|
void Client::SendMercTimerPacket(int32 entity_id, int32 merc_state, int32 suspended_time, int32 update_interval, int32 unk01) {
|
|
|
|
// Send Mercenary Status/Timer packet
|
|
auto outapp = new EQApplicationPacket(OP_MercenaryTimer, sizeof(MercenaryStatus_Struct));
|
|
MercenaryStatus_Struct* mss = (MercenaryStatus_Struct*)outapp->pBuffer;
|
|
mss->MercEntityID = entity_id; // Seen 0 (no merc spawned) or unknown value when merc is spawned
|
|
mss->MercState = merc_state; // Seen 5 (normal) or 1 (suspended)
|
|
mss->SuspendedTime = suspended_time; // Seen 0 for not suspended or Unix Timestamp for suspended merc
|
|
mss->UpdateInterval = update_interval; // Seen 900000 - 15 minutes in ms
|
|
mss->MercUnk01 = unk01; // Seen 180000 - 3 minutes in ms - Used for the unsuspend button refresh timer
|
|
FastQueuePacket(&outapp);
|
|
Log(Logs::Detail, Logs::Mercenaries, "Sent SendMercTimerPacket EndID: %i, State: %i, SuspendTime: %i, Interval: %i, Unk1: %i, Client: %s.", entity_id, merc_state, suspended_time, update_interval, unk01, GetName());
|
|
|
|
}
|
|
|
|
void Client::SendMercAssignPacket(uint32 entityID, uint32 unk01, uint32 unk02) {
|
|
auto outapp = new EQApplicationPacket(OP_MercenaryAssign, sizeof(MercenaryAssign_Struct));
|
|
MercenaryAssign_Struct* mas = (MercenaryAssign_Struct*)outapp->pBuffer;
|
|
mas->MercEntityID = entityID;
|
|
mas->MercUnk01 = unk01;
|
|
mas->MercUnk02 = unk02;
|
|
FastQueuePacket(&outapp);
|
|
Log(Logs::Detail, Logs::Mercenaries, "Sent SendMercAssignPacket EndID: %i, Unk1: %i, Unk2: %i, Client: %s.", entityID, unk01, unk02, GetName());
|
|
}
|
|
|
|
void NPC::LoadMercenaryTypes()
|
|
{
|
|
const std::string& query = fmt::format(
|
|
SQL(
|
|
SELECT DISTINCT MTyp.dbstring, MTyp.clientversion
|
|
FROM merc_merchant_entries MME, merc_merchant_template_entries MMTE,
|
|
merc_types MTyp, merc_templates MTem
|
|
WHERE MME.merchant_id = {}
|
|
AND MME.merc_merchant_template_id = MMTE.merc_merchant_template_id
|
|
AND MMTE.merc_template_id = MTem.merc_template_id
|
|
AND MTem.merc_type_id = MTyp.merc_type_id
|
|
),
|
|
GetNPCTypeID()
|
|
);
|
|
|
|
auto results = database.QueryDatabase(query);
|
|
if (!results.Success() || !results.RowCount()) {
|
|
return;
|
|
}
|
|
|
|
for (auto row : results) {
|
|
mercTypeList.push_back(
|
|
MercType{
|
|
.Type = Strings::ToUnsignedInt(row[0]),
|
|
.ClientVersion = Strings::ToUnsignedInt(row[1])
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
void NPC::LoadMercenaries()
|
|
{
|
|
const std::string& query = fmt::format(
|
|
SQL(
|
|
SELECT DISTINCT MTem.merc_template_id, MTyp.dbstring AS merc_type_id,
|
|
MTem.dbstring AS merc_subtype_id, 0 AS CostFormula,
|
|
CASE WHEN MTem.clientversion > MTyp.clientversion
|
|
THEN MTem.clientversion
|
|
ELSE MTyp.clientversion END AS clientversion, MTem.merc_npc_type_id
|
|
FROM merc_merchant_entries MME, merc_merchant_template_entries MMTE,
|
|
merc_types MTyp, merc_templates MTem
|
|
WHERE MME.merchant_id = {} AND
|
|
MME.merc_merchant_template_id = MMTE.merc_merchant_template_id
|
|
AND MMTE.merc_template_id = MTem.merc_template_id
|
|
AND MTem.merc_type_id = MTyp.merc_type_id
|
|
),
|
|
GetNPCTypeID()
|
|
);
|
|
|
|
auto results = database.QueryDatabase(query);
|
|
if (!results.Success() || !results.RowCount()) {
|
|
return;
|
|
}
|
|
|
|
for (auto row : results) {
|
|
mercDataList.push_back(
|
|
MercData{
|
|
.MercTemplateID = Strings::ToUnsignedInt(row[0]),
|
|
.MercType = Strings::ToUnsignedInt(row[1]),
|
|
.MercSubType = Strings::ToUnsignedInt(row[2]),
|
|
.CostFormula = Strings::ToUnsignedInt(row[3]),
|
|
.ClientVersion = Strings::ToUnsignedInt(row[4]),
|
|
.NPCID = Strings::ToUnsignedInt(row[5])
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
int NPC::GetNumMercenaryTypes(uint32 clientVersion) {
|
|
|
|
int count = 0;
|
|
std::list<MercType> mercTypeList = GetMercenaryTypesList();
|
|
|
|
for (auto mercTypeListItr = mercTypeList.begin(); mercTypeListItr != mercTypeList.end(); ++mercTypeListItr) {
|
|
if(mercTypeListItr->ClientVersion <= clientVersion)
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
int NPC::GetNumberOfMercenaries(uint32 clientVersion) {
|
|
|
|
int count = 0;
|
|
std::list<MercData> mercDataList = GetMercenariesList();
|
|
|
|
for (auto mercListItr = mercDataList.begin(); mercListItr != mercDataList.end(); ++mercListItr) {
|
|
if(mercListItr->ClientVersion <= clientVersion)
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
std::list<MercType> NPC::GetMercenaryTypesList(uint32 clientVersion) {
|
|
|
|
std::list<MercType> result;
|
|
|
|
if(GetNumMercenaryTypes() > 0)
|
|
{
|
|
for (auto mercTypeListItr = mercTypeList.begin(); mercTypeListItr != mercTypeList.end();
|
|
++mercTypeListItr) {
|
|
if(mercTypeListItr->ClientVersion <= clientVersion)
|
|
{
|
|
MercType mercType;
|
|
mercType.Type = mercTypeListItr->Type;
|
|
mercType.ClientVersion = mercTypeListItr->ClientVersion;
|
|
result.push_back(mercType);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::list<MercData> NPC::GetMercenariesList(uint32 clientVersion) {
|
|
|
|
std::list<MercData> result;
|
|
|
|
if(GetNumberOfMercenaries() > 0)
|
|
{
|
|
for (auto mercListItr = mercDataList.begin(); mercListItr != mercDataList.end(); ++mercListItr) {
|
|
if(mercListItr->ClientVersion <= clientVersion)
|
|
{
|
|
MercTemplate *merc_template = zone->GetMercTemplate(mercListItr->MercTemplateID);
|
|
|
|
if(merc_template)
|
|
{
|
|
MercData mercData;
|
|
mercData.MercTemplateID = mercListItr->MercTemplateID;
|
|
mercData.MercType = merc_template->MercType;
|
|
mercData.MercSubType = merc_template->MercSubType;
|
|
mercData.CostFormula = merc_template->CostFormula;
|
|
mercData.ClientVersion = merc_template->ClientVersion;
|
|
mercData.NPCID = merc_template->MercNPCID;
|
|
result.push_back(mercData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
uint32 Merc::CalcPurchaseCost(uint32 templateID , uint8 level, uint8 currency_type) {
|
|
|
|
uint32 cost = 0;
|
|
|
|
MercTemplate *mercData = zone->GetMercTemplate(templateID);
|
|
|
|
if(mercData)
|
|
{
|
|
//calculate cost in coin - cost in gold
|
|
if(currency_type == 0)
|
|
{
|
|
int levels_above_cutoff;
|
|
switch (mercData->CostFormula)
|
|
{
|
|
case 0:
|
|
levels_above_cutoff = level > 10 ? (level - 10) : 0;
|
|
cost = levels_above_cutoff * 300;
|
|
cost += level >= 10 ? 100 : 0;
|
|
cost /= 100;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if(currency_type == 19)
|
|
{
|
|
// cost in Bayle Marks
|
|
cost = 1;
|
|
}
|
|
}
|
|
|
|
return cost;
|
|
}
|
|
|
|
uint32 Merc::CalcUpkeepCost(uint32 templateID , uint8 level, uint8 currency_type) {
|
|
|
|
uint32 cost = 0;
|
|
|
|
MercTemplate *mercData = zone->GetMercTemplate(templateID);
|
|
|
|
if(mercData)
|
|
{
|
|
//calculate cost in coin - cost in gold
|
|
if(currency_type == 0)
|
|
{
|
|
int levels_above_cutoff;
|
|
switch (mercData->CostFormula)
|
|
{
|
|
case 0:
|
|
levels_above_cutoff = level > 10 ? (level - 10) : 0;
|
|
cost = levels_above_cutoff * 300;
|
|
cost += level >= 10 ? 100 : 0;
|
|
cost /= 100;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if(currency_type == 19)
|
|
{
|
|
// cost in Bayle Marks
|
|
cost = 1;
|
|
}
|
|
}
|
|
|
|
return cost;
|
|
}
|
|
|
|
void Merc::Signal(int signal_id)
|
|
{
|
|
if (parse->MercHasQuestSub(EVENT_SIGNAL)) {
|
|
parse->EventMerc(EVENT_SIGNAL, this, nullptr, std::to_string(signal_id), 0);
|
|
}
|
|
}
|
|
|
|
void Merc::SendPayload(int payload_id, std::string payload_value)
|
|
{
|
|
if (parse->MercHasQuestSub(EVENT_PAYLOAD)) {
|
|
const auto& export_string = fmt::format("{} {}", payload_id, payload_value);
|
|
|
|
parse->EventMerc(EVENT_PAYLOAD, this, nullptr, export_string, 0);
|
|
}
|
|
}
|