mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 16:51: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>
4266 lines
108 KiB
C++
4266 lines
108 KiB
C++
/* EQEMu: Everquest Server Emulator
|
|
Copyright (C) 2001-2002 EQEMu Development Team (http://eqemu.org)
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; version 2 of the License.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY except by those people which sell it, which
|
|
are required to give you total support for your newly bought product;
|
|
without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include "../common/bodytypes.h"
|
|
#include "../common/classes.h"
|
|
#include "../common/global_define.h"
|
|
#include "../common/misc_functions.h"
|
|
#include "../common/rulesys.h"
|
|
#include "../common/seperator.h"
|
|
#include "../common/spdat.h"
|
|
#include "../common/strings.h"
|
|
#include "../common/emu_versions.h"
|
|
#include "../common/features.h"
|
|
#include "../common/item_instance.h"
|
|
#include "../common/linked_list.h"
|
|
#include "../common/servertalk.h"
|
|
#include "../common/say_link.h"
|
|
#include "../common/data_verification.h"
|
|
|
|
#include "../common/repositories/npc_types_repository.h"
|
|
#include "../common/repositories/spawngroup_repository.h"
|
|
#include "../common/repositories/spawn2_repository.h"
|
|
#include "../common/repositories/spawnentry_repository.h"
|
|
|
|
#include "client.h"
|
|
#include "entity.h"
|
|
#include "npc.h"
|
|
#include "string_ids.h"
|
|
#include "spawn2.h"
|
|
#include "zone.h"
|
|
#include "quest_parser_collection.h"
|
|
#include "water_map.h"
|
|
#include "npc_scale_manager.h"
|
|
|
|
#include "bot.h"
|
|
#include "../common/skill_caps.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#ifdef _WINDOWS
|
|
#define snprintf _snprintf
|
|
#define strncasecmp _strnicmp
|
|
#define strcasecmp _stricmp
|
|
#else
|
|
#include <stdlib.h>
|
|
#include <pthread.h>
|
|
#endif
|
|
|
|
extern Zone* zone;
|
|
extern volatile bool is_zone_loaded;
|
|
extern EntityList entity_list;
|
|
|
|
NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &position, GravityBehavior iflymode, bool IsCorpse)
|
|
: Mob(
|
|
npc_type_data->name,
|
|
npc_type_data->lastname,
|
|
npc_type_data->max_hp,
|
|
npc_type_data->max_hp,
|
|
npc_type_data->gender,
|
|
npc_type_data->race,
|
|
npc_type_data->class_,
|
|
npc_type_data->bodytype,
|
|
npc_type_data->deity,
|
|
npc_type_data->level,
|
|
npc_type_data->npc_id,
|
|
npc_type_data->size,
|
|
npc_type_data->runspeed,
|
|
position,
|
|
npc_type_data->light, // innate_light
|
|
npc_type_data->texture,
|
|
npc_type_data->helmtexture,
|
|
npc_type_data->AC,
|
|
npc_type_data->ATK,
|
|
npc_type_data->STR,
|
|
npc_type_data->STA,
|
|
npc_type_data->DEX,
|
|
npc_type_data->AGI,
|
|
npc_type_data->INT,
|
|
npc_type_data->WIS,
|
|
npc_type_data->CHA,
|
|
npc_type_data->haircolor,
|
|
npc_type_data->beardcolor,
|
|
npc_type_data->eyecolor1,
|
|
npc_type_data->eyecolor2,
|
|
npc_type_data->hairstyle,
|
|
npc_type_data->luclinface,
|
|
npc_type_data->beard,
|
|
npc_type_data->drakkin_heritage,
|
|
npc_type_data->drakkin_tattoo,
|
|
npc_type_data->drakkin_details,
|
|
npc_type_data->armor_tint,
|
|
0,
|
|
npc_type_data->see_invis, // pass see_invis/see_ivu flags to mob constructor
|
|
npc_type_data->see_invis_undead,
|
|
npc_type_data->see_hide,
|
|
npc_type_data->see_improved_hide,
|
|
npc_type_data->hp_regen,
|
|
npc_type_data->mana_regen,
|
|
npc_type_data->qglobal,
|
|
npc_type_data->maxlevel,
|
|
npc_type_data->scalerate,
|
|
npc_type_data->armtexture,
|
|
npc_type_data->bracertexture,
|
|
npc_type_data->handtexture,
|
|
npc_type_data->legtexture,
|
|
npc_type_data->feettexture,
|
|
npc_type_data->use_model,
|
|
npc_type_data->always_aggro,
|
|
npc_type_data->heroic_strikethrough,
|
|
npc_type_data->keeps_sold_items,
|
|
npc_type_data->hp_regen_per_second
|
|
),
|
|
attacked_timer(CombatEventTimer_expire),
|
|
swarm_timer(100),
|
|
classattack_timer(1000),
|
|
monkattack_timer(1000),
|
|
knightattack_timer(1000),
|
|
assist_timer(AIassistcheck_delay),
|
|
qglobal_purge_timer(30000),
|
|
send_hp_update_timer(2000),
|
|
enraged_timer(1000),
|
|
taunt_timer(TauntReuseTime * 1000),
|
|
m_SpawnPoint(position),
|
|
m_GuardPoint(-1, -1, -1, 0),
|
|
m_GuardPointSaved(0, 0, 0, 0)
|
|
{
|
|
//What is the point of this, since the names get mangled..
|
|
Mob *mob = entity_list.GetMob(name);
|
|
if (mob != nullptr) {
|
|
entity_list.RemoveEntity(mob->GetID());
|
|
}
|
|
|
|
int moblevel = GetLevel();
|
|
|
|
NPCTypedata = npc_type_data;
|
|
NPCTypedata_ours = nullptr;
|
|
respawn2 = in_respawn;
|
|
|
|
swarm_timer.Disable();
|
|
|
|
if (size <= 0.0f) {
|
|
size = GetRaceGenderDefaultHeight(race, gender);
|
|
}
|
|
|
|
// lava dragon is a fixed size model and should always use its default
|
|
// otherwise pathing issues
|
|
if (race == Race::LavaDragon) {
|
|
size = 5;
|
|
}
|
|
if (race == Race::Wurm) {
|
|
size = 15;
|
|
}
|
|
|
|
SetTaunting(false);
|
|
proximity = nullptr;
|
|
m_loot_copper = 0;
|
|
m_loot_silver = 0;
|
|
m_loot_gold = 0;
|
|
m_loot_platinum = 0;
|
|
max_dmg = npc_type_data->max_dmg;
|
|
min_dmg = npc_type_data->min_dmg;
|
|
attack_count = npc_type_data->attack_count;
|
|
grid = 0;
|
|
wp_m = 0;
|
|
max_wp = 0;
|
|
save_wp = 0;
|
|
spawn_group_id = 0;
|
|
swarmInfoPtr = nullptr;
|
|
spellscale = npc_type_data->spellscale;
|
|
healscale = npc_type_data->healscale;
|
|
pAggroRange = npc_type_data->aggroradius;
|
|
pAssistRange = npc_type_data->assistradius;
|
|
findable = npc_type_data->findable;
|
|
trackable = npc_type_data->trackable;
|
|
MR = npc_type_data->MR;
|
|
CR = npc_type_data->CR;
|
|
DR = npc_type_data->DR;
|
|
FR = npc_type_data->FR;
|
|
PR = npc_type_data->PR;
|
|
Corrup = npc_type_data->Corrup;
|
|
PhR = npc_type_data->PhR;
|
|
STR = npc_type_data->STR;
|
|
STA = npc_type_data->STA;
|
|
AGI = npc_type_data->AGI;
|
|
DEX = npc_type_data->DEX;
|
|
INT = npc_type_data->INT;
|
|
WIS = npc_type_data->WIS;
|
|
CHA = npc_type_data->CHA;
|
|
npc_mana = npc_type_data->Mana;
|
|
m_is_underwater_only = npc_type_data->underwater;
|
|
m_is_quest_npc = npc_type_data->is_quest_npc;
|
|
|
|
//quick fix of ordering if they screwed it up in the DB
|
|
if (max_dmg < min_dmg) {
|
|
int tmp = min_dmg;
|
|
min_dmg = max_dmg;
|
|
max_dmg = tmp;
|
|
}
|
|
|
|
// Max Level and Stat Scaling if maxlevel is set
|
|
if (maxlevel > level) {
|
|
LevelScale();
|
|
}
|
|
|
|
base_damage = round((max_dmg - min_dmg) / 1.9);
|
|
min_damage = min_dmg - round(base_damage / 10.0);
|
|
accuracy_rating = npc_type_data->accuracy_rating;
|
|
avoidance_rating = npc_type_data->avoidance_rating;
|
|
ATK = npc_type_data->ATK;
|
|
heroic_strikethrough = npc_type_data->heroic_strikethrough;
|
|
keeps_sold_items = npc_type_data->keeps_sold_items;
|
|
|
|
// used for when switch back to charm
|
|
default_ac = npc_type_data->AC;
|
|
default_min_dmg = min_dmg;
|
|
default_max_dmg = max_dmg;
|
|
default_attack_delay = npc_type_data->attack_delay;
|
|
default_accuracy_rating = npc_type_data->accuracy_rating;
|
|
default_avoidance_rating = npc_type_data->avoidance_rating;
|
|
default_atk = npc_type_data->ATK;
|
|
strn0cpy(default_special_abilities, npc_type_data->special_abilities, 512);
|
|
|
|
// used for when getting charmed, if 0, doesn't swap
|
|
charm_ac = npc_type_data->charm_ac;
|
|
charm_min_dmg = npc_type_data->charm_min_dmg;
|
|
charm_max_dmg = npc_type_data->charm_max_dmg;
|
|
charm_attack_delay = npc_type_data->charm_attack_delay;
|
|
charm_accuracy_rating = npc_type_data->charm_accuracy_rating;
|
|
charm_avoidance_rating = npc_type_data->charm_avoidance_rating;
|
|
charm_atk = npc_type_data->charm_atk;
|
|
|
|
CalcMaxMana();
|
|
RestoreMana();
|
|
|
|
MerchantType = npc_type_data->merchanttype;
|
|
merchant_open = (
|
|
GetClass() == Class::Merchant ||
|
|
GetClass() == Class::DiscordMerchant ||
|
|
GetClass() == Class::AdventureMerchant ||
|
|
GetClass() == Class::NorrathsKeepersMerchant ||
|
|
GetClass() == Class::DarkReignMerchant ||
|
|
GetClass() == Class::AlternateCurrencyMerchant
|
|
);
|
|
adventure_template_id = npc_type_data->adventure_template;
|
|
flymode = iflymode;
|
|
|
|
// If server has set a flymode in db honor it over all else.
|
|
// If server has not set a flymde in db, and this is a boat - force floating.
|
|
if (npc_type_data->flymode >= 0) {
|
|
flymode = static_cast<GravityBehavior>(npc_type_data->flymode);
|
|
}
|
|
else if (GetIsBoat()) {
|
|
flymode = GravityBehavior::Floating;
|
|
}
|
|
|
|
guard_anim = eaStanding;
|
|
|
|
m_roambox.max_x = -2;
|
|
m_roambox.max_y = -2;
|
|
m_roambox.min_x = -2;
|
|
m_roambox.min_y = -2;
|
|
m_roambox.distance = 0;
|
|
m_roambox.dest_x = -2;
|
|
m_roambox.dest_y = -2;
|
|
m_roambox.dest_z = 0;
|
|
m_roambox.delay = 1000;
|
|
m_roambox.min_delay = 1000;
|
|
|
|
p_depop = false;
|
|
m_loottable_id = npc_type_data->loottable_id;
|
|
m_skip_global_loot = npc_type_data->skip_global_loot;
|
|
m_skip_auto_scale = npc_type_data->skip_auto_scale;
|
|
rare_spawn = npc_type_data->rare_spawn;
|
|
no_target_hotkey = npc_type_data->no_target_hotkey;
|
|
primary_faction = 0;
|
|
faction_amount = npc_type_data->faction_amount;
|
|
|
|
SetNPCFactionID(npc_type_data->npc_faction_id);
|
|
|
|
npc_spells_id = 0;
|
|
HasAISpell = false;
|
|
HasAISpellEffects = false;
|
|
innate_proc_spell_id = 0;
|
|
m_record_loot_stats = false;
|
|
|
|
if (GetClass() == Class::MercenaryLiaison && RuleB(Mercs, AllowMercs)) {
|
|
LoadMercenaryTypes();
|
|
LoadMercenaries();
|
|
}
|
|
|
|
SpellFocusDMG = 0;
|
|
SpellFocusHeal = 0;
|
|
|
|
pet_spell_id = 0;
|
|
|
|
delaytimer = false;
|
|
combat_event = false;
|
|
attack_speed = npc_type_data->attack_speed;
|
|
attack_delay = npc_type_data->attack_delay;
|
|
slow_mitigation = npc_type_data->slow_mitigation;
|
|
|
|
EntityList::RemoveNumbers(name);
|
|
entity_list.MakeNameUnique(name);
|
|
|
|
npc_aggro = npc_type_data->npc_aggro;
|
|
|
|
AISpellVar.fail_recast = static_cast<uint32>(RuleI(Spells, AI_SpellCastFinishedFailRecast));
|
|
AISpellVar.engaged_no_sp_recast_min = static_cast<uint32>(RuleI(Spells, AI_EngagedNoSpellMinRecast));
|
|
AISpellVar.engaged_no_sp_recast_max = static_cast<uint32>(RuleI(Spells, AI_EngagedNoSpellMaxRecast));
|
|
AISpellVar.engaged_beneficial_self_chance = static_cast<uint8> (RuleI(Spells, AI_EngagedBeneficialSelfChance));
|
|
AISpellVar.engaged_beneficial_other_chance = static_cast<uint8> (RuleI(Spells, AI_EngagedBeneficialOtherChance));
|
|
AISpellVar.engaged_detrimental_chance = static_cast<uint8> (RuleI(Spells, AI_EngagedDetrimentalChance));
|
|
AISpellVar.pursue_no_sp_recast_min = static_cast<uint32>(RuleI(Spells, AI_PursueNoSpellMinRecast));
|
|
AISpellVar.pursue_no_sp_recast_max = static_cast<uint32>(RuleI(Spells, AI_PursueNoSpellMaxRecast));
|
|
AISpellVar.pursue_detrimental_chance = static_cast<uint8> (RuleI(Spells, AI_PursueDetrimentalChance));
|
|
AISpellVar.idle_no_sp_recast_min = static_cast<uint32>(RuleI(Spells, AI_IdleNoSpellMinRecast));
|
|
AISpellVar.idle_no_sp_recast_max = static_cast<uint32>(RuleI(Spells, AI_IdleNoSpellMaxRecast));
|
|
AISpellVar.idle_beneficial_chance = static_cast<uint8> (RuleI(Spells, AI_IdleBeneficialChance));
|
|
|
|
// It's possible for IsBot() to not be set yet during Bot loading, so have to use an alternative to catch Bots
|
|
if (!EQ::ValueWithin(npc_type_data->npc_spells_id, EQ::constants::BotSpellIDs::Warrior, EQ::constants::BotSpellIDs::Berserker)) {
|
|
AI_Init();
|
|
AI_Start();
|
|
} else {
|
|
CastToBot()->AI_Bot_Init();
|
|
CastToBot()->AI_Bot_Start();
|
|
}
|
|
|
|
d_melee_texture1 = npc_type_data->d_melee_texture1;
|
|
d_melee_texture2 = npc_type_data->d_melee_texture2;
|
|
herosforgemodel = npc_type_data->herosforgemodel;
|
|
|
|
ammo_idfile = npc_type_data->ammo_idfile;
|
|
memset(equipment, 0, sizeof(equipment));
|
|
prim_melee_type = npc_type_data->prim_melee_type;
|
|
sec_melee_type = npc_type_data->sec_melee_type;
|
|
ranged_type = npc_type_data->ranged_type;
|
|
|
|
// If Melee Textures are not set, set attack type to Hand to Hand as default
|
|
if (!d_melee_texture1) {
|
|
prim_melee_type = 28;
|
|
}
|
|
if (!d_melee_texture2) {
|
|
sec_melee_type = 28;
|
|
}
|
|
|
|
//give NPCs skill values...
|
|
int r;
|
|
for (r = 0; r <= EQ::skills::HIGHEST_SKILL; r++) {
|
|
skills[r] = skill_caps.GetSkillCap(GetClass(), (EQ::skills::SkillType)r, moblevel).cap;
|
|
}
|
|
// some overrides -- really we need to be able to set skills for mobs in the DB
|
|
// There are some known low level SHM/BST pets that do not follow this, which supports
|
|
// the theory of needing to be able to set skills for each mob separately
|
|
if (IsBot()) {
|
|
if (GetClass() != Class::Paladin && GetClass() != Class::ShadowKnight) {
|
|
knightattack_timer.Disable();
|
|
}
|
|
else if (GetClass() != Class::Monk || GetLevel() < 10) {
|
|
monkattack_timer.Disable();
|
|
}
|
|
}
|
|
else {
|
|
if (moblevel > 50) {
|
|
skills[EQ::skills::SkillDoubleAttack] = 250;
|
|
skills[EQ::skills::SkillDualWield] = 250;
|
|
}
|
|
else if (moblevel > 3) {
|
|
skills[EQ::skills::SkillDoubleAttack] = moblevel * 5;
|
|
skills[EQ::skills::SkillDualWield] = skills[EQ::skills::SkillDoubleAttack];
|
|
}
|
|
else {
|
|
skills[EQ::skills::SkillDoubleAttack] = moblevel * 5;
|
|
}
|
|
}
|
|
|
|
SetLDoNTrapped(false);
|
|
SetLDoNTrapType(0);
|
|
SetLDoNTrapSpellID(0);
|
|
SetLDoNLocked(false);
|
|
SetLDoNLockedSkill(0);
|
|
SetLDoNTrapDetected(false);
|
|
|
|
if (npc_type_data->trap_template > 0) {
|
|
std::map<uint32, std::list<LDoNTrapTemplate *> >::iterator trap_ent_iter;
|
|
std::list<LDoNTrapTemplate *> trap_list;
|
|
|
|
trap_ent_iter = zone->ldon_trap_entry_list.find(npc_type_data->trap_template);
|
|
if (trap_ent_iter != zone->ldon_trap_entry_list.end()) {
|
|
trap_list = trap_ent_iter->second;
|
|
if (trap_list.size() > 0) {
|
|
auto trap_list_iter = trap_list.begin();
|
|
std::advance(trap_list_iter, zone->random.Int(0, trap_list.size() - 1));
|
|
LDoNTrapTemplate *trap_template = (*trap_list_iter);
|
|
if (trap_template) {
|
|
if (trap_template->spell_id > 0) {
|
|
SetLDoNTrapped(true);
|
|
SetLDoNTrapSpellID(trap_template->spell_id);
|
|
} else {
|
|
SetLDoNTrapped(false);
|
|
SetLDoNTrapSpellID(0);
|
|
}
|
|
|
|
SetLDoNTrapType(static_cast<uint8>(trap_template->type));
|
|
|
|
if (trap_template->locked > 0) {
|
|
SetLDoNLocked(true);
|
|
SetLDoNLockedSkill(trap_template->skill);
|
|
} else {
|
|
SetLDoNLocked(false);
|
|
SetLDoNLockedSkill(0);
|
|
}
|
|
|
|
SetLDoNTrapDetected(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
reface_timer = new Timer(15000);
|
|
reface_timer->Disable();
|
|
|
|
qGlobals = nullptr;
|
|
|
|
SetEmoteID(npc_type_data->emoteid);
|
|
InitializeBuffSlots();
|
|
CalcBonuses();
|
|
|
|
raid_target = npc_type_data->raid_target;
|
|
ignore_despawn = npc_type_data->ignore_despawn;
|
|
m_targetable = !npc_type_data->untargetable;
|
|
|
|
npc_scale_manager->ScaleNPC(this);
|
|
|
|
RestoreMana();
|
|
|
|
if (GetBodyType() == BodyType::Animal && !RuleB(NPC, AnimalsOpenDoors)) {
|
|
m_can_open_doors = false;
|
|
}
|
|
|
|
GetInv().SetInventoryVersion(EQ::versions::MobVersion::RoF2);
|
|
GetInv().SetGMInventory(false);
|
|
}
|
|
|
|
float NPC::GetRoamboxMaxX() const
|
|
{
|
|
return m_roambox.max_x;
|
|
}
|
|
|
|
float NPC::GetRoamboxMaxY() const
|
|
{
|
|
return m_roambox.max_y;
|
|
}
|
|
|
|
float NPC::GetRoamboxMinX() const
|
|
{
|
|
return m_roambox.min_x;
|
|
}
|
|
|
|
float NPC::GetRoamboxMinY() const
|
|
{
|
|
return m_roambox.min_y;
|
|
}
|
|
|
|
float NPC::GetRoamboxDistance() const
|
|
{
|
|
return m_roambox.distance;
|
|
}
|
|
|
|
float NPC::GetRoamboxDestinationX() const
|
|
{
|
|
return m_roambox.dest_x;
|
|
}
|
|
|
|
float NPC::GetRoamboxDestinationY() const
|
|
{
|
|
return m_roambox.dest_y;
|
|
}
|
|
|
|
float NPC::GetRoamboxDestinationZ() const
|
|
{
|
|
return m_roambox.dest_z;
|
|
}
|
|
|
|
uint32 NPC::GetRoamboxDelay() const
|
|
{
|
|
return m_roambox.delay;
|
|
}
|
|
|
|
uint32 NPC::GetRoamboxMinDelay() const
|
|
{
|
|
return m_roambox.min_delay;
|
|
}
|
|
|
|
NPC::~NPC()
|
|
{
|
|
AI_Stop();
|
|
|
|
if (proximity) {
|
|
entity_list.RemoveProximity(GetID());
|
|
safe_delete(proximity);
|
|
}
|
|
|
|
safe_delete(NPCTypedata_ours);
|
|
|
|
LootItems::iterator cur, end;
|
|
cur = m_loot_items.begin();
|
|
end = m_loot_items.end();
|
|
for (; cur != end; ++cur) {
|
|
LootItem *item = *cur;
|
|
safe_delete(item);
|
|
}
|
|
|
|
m_loot_items.clear();
|
|
faction_list.clear();
|
|
|
|
safe_delete(reface_timer);
|
|
safe_delete(swarmInfoPtr);
|
|
safe_delete(qGlobals);
|
|
UninitializeBuffSlots();
|
|
}
|
|
|
|
void NPC::SetTarget(Mob* mob) {
|
|
if(mob == GetTarget()) //dont bother if they are allready our target
|
|
return;
|
|
|
|
if (GetPetTargetLockID()) {
|
|
TryDepopTargetLockedPets(mob);
|
|
}
|
|
|
|
if (mob) {
|
|
SetAttackTimer();
|
|
} else {
|
|
ranged_timer.Disable();
|
|
//attack_timer.Disable();
|
|
attack_dw_timer.Disable();
|
|
}
|
|
|
|
// either normal pet and owner is client or charmed pet and owner is client
|
|
Mob *owner = nullptr;
|
|
if (IsPet() && IsPetOwnerClient()) {
|
|
owner = GetOwner();
|
|
} else if (IsCharmed()) {
|
|
owner = GetOwner();
|
|
if (owner && !owner->IsClient())
|
|
owner = nullptr;
|
|
}
|
|
|
|
if (owner) {
|
|
auto client = owner->CastToClient();
|
|
if (client->ClientVersionBit() & EQ::versions::maskUFAndLater) {
|
|
auto app = new EQApplicationPacket(OP_PetHoTT, sizeof(ClientTarget_Struct));
|
|
auto ct = (ClientTarget_Struct *)app->pBuffer;
|
|
ct->new_target = mob ? mob->GetID() : 0;
|
|
client->FastQueuePacket(&app);
|
|
}
|
|
}
|
|
Mob::SetTarget(mob);
|
|
}
|
|
|
|
bool NPC::Process()
|
|
{
|
|
if (p_depop)
|
|
{
|
|
Mob* owner = entity_list.GetMob(ownerid);
|
|
if (owner != 0)
|
|
{
|
|
//if(GetBodyType() != BodyType::SwarmPet)
|
|
// owner->SetPetID(0);
|
|
ownerid = 0;
|
|
petid = 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (IsStunned() && stunned_timer.Check()) {
|
|
Mob::UnStun();
|
|
spun_timer.Disable();
|
|
}
|
|
|
|
SpellProcess();
|
|
|
|
if (swarm_timer.Check()) {
|
|
DepopSwarmPets();
|
|
}
|
|
|
|
if (m_scan_close_mobs_timer.Check()) {
|
|
entity_list.ScanCloseMobs(this);
|
|
}
|
|
|
|
if (m_mob_check_moving_timer.Check()) {
|
|
CheckScanCloseMobsMovingTimer();
|
|
}
|
|
|
|
if (hp_regen_per_second > 0 && hp_regen_per_second_timer.Check()) {
|
|
if (GetHP() < GetMaxHP()) {
|
|
SetHP(GetHP() + hp_regen_per_second);
|
|
}
|
|
}
|
|
|
|
if (tic_timer.Check()) {
|
|
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_TICK)) {
|
|
parse->EventNPC(EVENT_TICK, this, nullptr, "", 0);
|
|
}
|
|
|
|
BuffProcess();
|
|
|
|
if (currently_fleeing) {
|
|
ProcessFlee();
|
|
}
|
|
|
|
int64 npc_sitting_regen_bonus = 0;
|
|
int64 pet_regen_bonus = 0;
|
|
int64 npc_regen = 0;
|
|
int64 npc_hp_regen = GetNPCHPRegen();
|
|
|
|
if (GetAppearance() == eaSitting) {
|
|
npc_sitting_regen_bonus += 3;
|
|
}
|
|
|
|
int64 ooc_regen_calc = 0;
|
|
if (ooc_regen > 0) { //should pull from Mob class
|
|
ooc_regen_calc += GetMaxHP() * ooc_regen / 100;
|
|
}
|
|
|
|
/**
|
|
* Use max value between two values
|
|
*/
|
|
npc_regen = std::max(npc_hp_regen, ooc_regen_calc);
|
|
|
|
if ((GetHP() < GetMaxHP()) && !IsPet()) {
|
|
if (!IsEngaged()) {
|
|
SetHP(GetHP() + npc_regen + npc_sitting_regen_bonus);
|
|
}
|
|
else {
|
|
SetHP(GetHP() + npc_hp_regen);
|
|
}
|
|
}
|
|
else if (GetHP() < GetMaxHP() && GetOwnerID() != 0) {
|
|
if (!IsEngaged()) {
|
|
if (ooc_regen > 0) {
|
|
pet_regen_bonus = std::max(ooc_regen_calc, npc_hp_regen);
|
|
}
|
|
else {
|
|
pet_regen_bonus = npc_hp_regen + (GetLevel() / 5);
|
|
}
|
|
|
|
SetHP(GetHP() + npc_sitting_regen_bonus + pet_regen_bonus);
|
|
}
|
|
else {
|
|
SetHP(GetHP() + npc_hp_regen);
|
|
}
|
|
|
|
}
|
|
else {
|
|
SetHP(GetHP() + npc_hp_regen + npc_sitting_regen_bonus);
|
|
}
|
|
|
|
if (GetMana() < GetMaxMana()) {
|
|
if (RuleB(NPC, UseMeditateBasedManaRegen)) {
|
|
int64 npc_idle_mana_regen_bonus = 2;
|
|
uint16 meditate_skill = GetSkill(EQ::skills::SkillMeditate);
|
|
if (!IsEngaged() && meditate_skill > 0) {
|
|
uint8 clevel = GetLevel();
|
|
npc_idle_mana_regen_bonus =
|
|
(((meditate_skill / 10) +
|
|
(clevel - (clevel / 4))) / 4) + 4;
|
|
}
|
|
SetMana(GetMana() + mana_regen + npc_idle_mana_regen_bonus);
|
|
}
|
|
else {
|
|
SetMana(GetMana() + mana_regen + npc_sitting_regen_bonus);
|
|
}
|
|
}
|
|
|
|
SendHPUpdate();
|
|
|
|
if (zone->adv_data && !p_depop) {
|
|
ServerZoneAdventureDataReply_Struct *ds = (ServerZoneAdventureDataReply_Struct *) zone->adv_data;
|
|
if (ds->type == Adventure_Rescue && ds->data_id == GetNPCTypeID()) {
|
|
Mob *o = GetOwner();
|
|
if (o && o->IsClient()) {
|
|
float x_diff = ds->dest_x - GetX();
|
|
float y_diff = ds->dest_y - GetY();
|
|
float z_diff = ds->dest_z - GetZ();
|
|
float dist = ((x_diff * x_diff) + (y_diff * y_diff) + (z_diff * z_diff));
|
|
if (dist < RuleR(Adventure, DistanceForRescueComplete)) {
|
|
zone->DoAdventureCountIncrease();
|
|
Say(
|
|
"You don't know what this means to me. Thank you so much for finding and saving me from"
|
|
" this wretched place. I'll find my way from here."
|
|
);
|
|
Depop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send HP updates when engaged
|
|
*/
|
|
if (send_hp_update_timer.Check(false) && IsEngaged()) {
|
|
SendHPUpdate();
|
|
}
|
|
|
|
if (viral_timer.Check()) {
|
|
VirusEffectProcess();
|
|
}
|
|
|
|
if(spellbonuses.GravityEffect == 1) {
|
|
if(gravity_timer.Check())
|
|
DoGravityEffect();
|
|
}
|
|
|
|
if(reface_timer->Check() && !IsEngaged() && IsPositionEqualWithinCertainZ(m_Position, m_GuardPoint, 5.0f)) {
|
|
RotateTo(m_GuardPoint.w);
|
|
reface_timer->Disable();
|
|
}
|
|
|
|
// needs to be done before mez and stun
|
|
if (ForcedMovement)
|
|
ProcessForcedMovement();
|
|
|
|
if (IsMezzed())
|
|
return true;
|
|
|
|
if(IsStunned()) {
|
|
if(spun_timer.Check())
|
|
Spin();
|
|
return true;
|
|
}
|
|
|
|
if (enraged_timer.Check()){
|
|
ProcessEnrage();
|
|
|
|
/* Don't keep running the check every second if we don't have enrage */
|
|
if (!GetSpecialAbility(SpecialAbility::Enrage)) {
|
|
enraged_timer.Disable();
|
|
}
|
|
}
|
|
|
|
//Handle assists...
|
|
if (assist_cap_timer.Check()) {
|
|
if (NPCAssistCap() > 0)
|
|
DelAssistCap();
|
|
else
|
|
assist_cap_timer.Disable();
|
|
}
|
|
|
|
if (assist_timer.Check() && IsEngaged() && !Charmed() && !HasAssistAggro() &&
|
|
NPCAssistCap() < RuleI(Combat, NPCAssistCap)) {
|
|
// Some cases like flash of light used for aggro haven't set target
|
|
if (!GetTarget()) {
|
|
SetTarget(hate_list.GetMobWithMostHateOnList(this));
|
|
}
|
|
AIYellForHelp(this, GetTarget());
|
|
if (NPCAssistCap() > 0 && !assist_cap_timer.Enabled())
|
|
assist_cap_timer.Start(RuleI(Combat, NPCAssistCapTimer));
|
|
}
|
|
|
|
if(qGlobals)
|
|
{
|
|
if(qglobal_purge_timer.Check())
|
|
{
|
|
qGlobals->PurgeExpiredGlobals();
|
|
}
|
|
}
|
|
|
|
if (bot_attack_flag_timer.Check()) {
|
|
bot_attack_flag_timer.Disable();
|
|
ClearBotAttackFlags();
|
|
}
|
|
|
|
AI_Process();
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32 NPC::CountLoot() {
|
|
return(m_loot_items.size());
|
|
}
|
|
|
|
void NPC::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 NPC::Depop(bool start_spawn_timer) {
|
|
if (emoteid) {
|
|
DoNPCEmote(EQ::constants::EmoteEventTypes::OnDespawn, emoteid);
|
|
}
|
|
|
|
parse->EventBotMercNPC(EVENT_DESPAWN, this, nullptr);
|
|
|
|
if (parse->HasQuestSub(ZONE_CONTROLLER_NPC_ID, EVENT_DESPAWN_ZONE)) {
|
|
DispatchZoneControllerEvent(EVENT_DESPAWN_ZONE, this, "", 0, nullptr);
|
|
}
|
|
|
|
p_depop = true;
|
|
if (respawn2) {
|
|
if (start_spawn_timer) {
|
|
respawn2->DeathReset();
|
|
} else {
|
|
respawn2->Depop();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NPC::SpawnZoneController()
|
|
{
|
|
|
|
if (!RuleB(Zone, UseZoneController))
|
|
return false;
|
|
|
|
auto npc_type = new NPCType;
|
|
memset(npc_type, 0, sizeof(NPCType));
|
|
|
|
strncpy(npc_type->name, "zone_controller", 60);
|
|
npc_type->current_hp = 2000000000;
|
|
npc_type->max_hp = 2000000000;
|
|
npc_type->hp_regen = 100000000;
|
|
npc_type->race = 240;
|
|
npc_type->size = .1;
|
|
npc_type->gender = 2;
|
|
npc_type->class_ = 1;
|
|
npc_type->deity = 1;
|
|
npc_type->level = 200;
|
|
npc_type->npc_id = ZONE_CONTROLLER_NPC_ID;
|
|
npc_type->loottable_id = 0;
|
|
npc_type->texture = 3;
|
|
npc_type->runspeed = 0;
|
|
npc_type->d_melee_texture1 = 0;
|
|
npc_type->d_melee_texture2 = 0;
|
|
npc_type->merchanttype = 0;
|
|
npc_type->bodytype = 11;
|
|
npc_type->skip_global_loot = true;
|
|
|
|
if (RuleB(Zone, EnableZoneControllerGlobals)) {
|
|
npc_type->qglobal = true;
|
|
}
|
|
|
|
npc_type->prim_melee_type = 28;
|
|
npc_type->sec_melee_type = 28;
|
|
|
|
npc_type->findable = 0;
|
|
npc_type->trackable = 0;
|
|
|
|
strcpy(npc_type->special_abilities, "12,1^13,1^14,1^15,1^16,1^17,1^19,1^22,1^24,1^25,1^28,1^31,1^35,1^39,1^42,1");
|
|
|
|
glm::vec4 point;
|
|
point.x = 3000;
|
|
point.y = 1000;
|
|
point.z = 500;
|
|
|
|
auto npc = new NPC(npc_type, nullptr, point, GravityBehavior::Flying);
|
|
npc->GiveNPCTypeData(npc_type);
|
|
|
|
entity_list.AddNPC(npc);
|
|
|
|
return true;
|
|
}
|
|
|
|
void NPC::SpawnGridNodeNPC(const glm::vec4 &position, int32 grid_id, int32 grid_number, int32 zoffset) {
|
|
auto npc_type = new NPCType;
|
|
memset(npc_type, 0, sizeof(NPCType));
|
|
|
|
std::string str_zoffset = Strings::NumberToWords(zoffset);
|
|
std::string str_number = Strings::NumberToWords(grid_number);
|
|
|
|
strcpy(npc_type->name, str_number.c_str());
|
|
if (zoffset != 0) {
|
|
strcat(npc_type->name, "(Stacked)");
|
|
}
|
|
|
|
npc_type->current_hp = 4000000;
|
|
npc_type->max_hp = 4000000;
|
|
npc_type->race = 2254;
|
|
npc_type->gender = Gender::Neuter;
|
|
npc_type->class_ = 9;
|
|
npc_type->deity = 1;
|
|
npc_type->level = 200;
|
|
npc_type->npc_id = 0;
|
|
npc_type->loottable_id = 0;
|
|
npc_type->texture = 1;
|
|
npc_type->light = 1;
|
|
npc_type->size = 1;
|
|
npc_type->runspeed = 0;
|
|
npc_type->merchanttype = 1;
|
|
npc_type->bodytype = 1;
|
|
npc_type->show_name = true;
|
|
npc_type->findable = true;
|
|
strn0cpy(npc_type->special_abilities, "24,1^35,1", 512);
|
|
|
|
auto node_position = glm::vec4(position.x, position.y, position.z, position.w);
|
|
auto npc = new NPC(npc_type, nullptr, node_position, GravityBehavior::Flying);
|
|
npc->name[strlen(npc->name) - 3] = (char) NULL;
|
|
npc->GiveNPCTypeData(npc_type);
|
|
npc->SetEntityVariable("grid_id", itoa(grid_id));
|
|
entity_list.AddNPC(npc);
|
|
}
|
|
|
|
NPC * NPC::SpawnZonePointNodeNPC(std::string name, const glm::vec4 &position)
|
|
{
|
|
auto npc_type = new NPCType;
|
|
memset(npc_type, 0, sizeof(NPCType));
|
|
|
|
char node_name[64];
|
|
strn0cpy(node_name, name.c_str(), 64);
|
|
|
|
strcpy(npc_type->name, entity_list.MakeNameUnique(node_name));
|
|
|
|
npc_type->current_hp = 4000000;
|
|
npc_type->max_hp = 4000000;
|
|
npc_type->race = 2254;
|
|
npc_type->gender = 2;
|
|
npc_type->class_ = 9;
|
|
npc_type->deity = 1;
|
|
npc_type->level = 200;
|
|
npc_type->npc_id = 0;
|
|
npc_type->loottable_id = 0;
|
|
npc_type->texture = 1;
|
|
npc_type->light = 1;
|
|
npc_type->size = 5;
|
|
npc_type->runspeed = 0;
|
|
npc_type->merchanttype = 1;
|
|
npc_type->bodytype = 1;
|
|
npc_type->show_name = true;
|
|
npc_type->findable = true;
|
|
|
|
strcpy(npc_type->special_abilities, "12,1^13,1^14,1^15,1^16,1^17,1^19,1^22,1^24,1^25,1^28,1^31,1^35,1^39,1^42,1");
|
|
|
|
auto node_position = glm::vec4(position.x, position.y, position.z, position.w);
|
|
auto npc = new NPC(npc_type, nullptr, node_position, GravityBehavior::Flying);
|
|
|
|
npc->name[strlen(npc->name)-3] = (char) NULL;
|
|
|
|
npc->GiveNPCTypeData(npc_type);
|
|
|
|
entity_list.AddNPC(npc);
|
|
|
|
return npc;
|
|
}
|
|
|
|
NPC * NPC::SpawnNodeNPC(std::string name, std::string last_name, const glm::vec4 &position) {
|
|
auto npc_type = new NPCType;
|
|
memset(npc_type, 0, sizeof(NPCType));
|
|
|
|
strncpy(npc_type->name, name.c_str(), 60);
|
|
|
|
npc_type->current_hp = 4000000;
|
|
npc_type->max_hp = 4000000;
|
|
npc_type->race = 127;
|
|
npc_type->gender = 0;
|
|
npc_type->class_ = 9;
|
|
npc_type->deity = 1;
|
|
npc_type->level = 200;
|
|
npc_type->npc_id = 0;
|
|
npc_type->loottable_id = 0;
|
|
npc_type->texture = 1;
|
|
npc_type->light = 1;
|
|
npc_type->size = 3;
|
|
npc_type->d_melee_texture1 = 1;
|
|
npc_type->d_melee_texture2 = 1;
|
|
npc_type->merchanttype = 1;
|
|
npc_type->bodytype = 1;
|
|
npc_type->show_name = true;
|
|
npc_type->findable = true;
|
|
npc_type->runspeed = 1.25;
|
|
|
|
strcpy(npc_type->special_abilities, "12,1^13,1^14,1^15,1^16,1^17,1^19,1^22,1^24,1^25,1^28,1^31,1^35,1^39,1^42,1");
|
|
|
|
auto node_position = glm::vec4(position.x, position.y, position.z, position.w);
|
|
auto npc = new NPC(npc_type, nullptr, node_position, GravityBehavior::Flying);
|
|
|
|
npc->name[strlen(npc->name) - 3] = (char) NULL;
|
|
|
|
npc->GiveNPCTypeData(npc_type);
|
|
|
|
entity_list.AddNPC(npc, true, true);
|
|
|
|
return npc;
|
|
}
|
|
|
|
NPC* NPC::SpawnNPC(const char* spawncommand, const glm::vec4& position, Client* client) {
|
|
if (spawncommand == 0 || spawncommand[0] == 0) {
|
|
return 0;
|
|
}
|
|
else {
|
|
Seperator sep(spawncommand);
|
|
//Lets see if someone didn't fill out the whole #spawn function properly
|
|
if (!sep.IsNumber(1)) {
|
|
sprintf(sep.arg[1], "1");
|
|
}
|
|
|
|
if (!sep.IsNumber(2)) {
|
|
sprintf(sep.arg[2], "1");
|
|
}
|
|
|
|
if (!sep.IsNumber(3)) {
|
|
sprintf(sep.arg[3], "0");
|
|
}
|
|
|
|
if (Strings::ToInt(sep.arg[4]) > 2100000000 || Strings::ToInt(sep.arg[4]) <= 0) {
|
|
sprintf(sep.arg[4], " ");
|
|
}
|
|
|
|
if (!strcmp(sep.arg[5], "-")) {
|
|
sprintf(sep.arg[5], " ");
|
|
}
|
|
|
|
if (!sep.IsNumber(5)) {
|
|
sprintf(sep.arg[5], " ");
|
|
}
|
|
|
|
if (!sep.IsNumber(6)) {
|
|
sprintf(sep.arg[6], "1");
|
|
}
|
|
|
|
if (!sep.IsNumber(8)) {
|
|
sprintf(sep.arg[8], "0");
|
|
}
|
|
|
|
if (!sep.IsNumber(9)) {
|
|
sprintf(sep.arg[9], "0");
|
|
}
|
|
|
|
if (!sep.IsNumber(7)) {
|
|
sprintf(sep.arg[7], "0");
|
|
}
|
|
|
|
if (!strcmp(sep.arg[4], "-")) {
|
|
sprintf(sep.arg[4], " ");
|
|
}
|
|
|
|
if (!sep.IsNumber(10)) { // bodytype
|
|
sprintf(sep.arg[10], "0");
|
|
}
|
|
|
|
//Calc MaxHP if client neglected to enter it...
|
|
if (sep.arg[4] && !sep.IsNumber(4)) {
|
|
sprintf(sep.arg[4], "1");
|
|
}
|
|
|
|
// Autoselect NPC Gender
|
|
if (sep.arg[5][0] == 0) {
|
|
sprintf(sep.arg[5], "%i", (int) Mob::GetDefaultGender(Strings::ToInt(sep.arg[1])));
|
|
}
|
|
|
|
//Time to create the NPC!!
|
|
auto npc_type = new NPCType;
|
|
memset(npc_type, 0, sizeof(NPCType));
|
|
|
|
strncpy(npc_type->name, sep.arg[0], 60);
|
|
|
|
npc_type->current_hp = Strings::ToInt(sep.arg[4]);
|
|
npc_type->max_hp = Strings::ToInt(sep.arg[4]);
|
|
npc_type->race = Strings::ToInt(sep.arg[1]);
|
|
npc_type->gender = Strings::ToInt(sep.arg[5]);
|
|
npc_type->class_ = Strings::ToInt(sep.arg[6]);
|
|
npc_type->deity = 1;
|
|
npc_type->level = Strings::ToInt(sep.arg[2]);
|
|
npc_type->npc_id = 0;
|
|
npc_type->loottable_id = 0;
|
|
npc_type->texture = Strings::ToInt(sep.arg[3]);
|
|
npc_type->light = 0;
|
|
npc_type->runspeed = 1.25f;
|
|
npc_type->d_melee_texture1 = Strings::ToUnsignedInt(sep.arg[7]);
|
|
npc_type->d_melee_texture2 = Strings::ToUnsignedInt(sep.arg[8]);
|
|
npc_type->merchanttype = Strings::ToInt(sep.arg[9]);
|
|
npc_type->bodytype = Strings::ToInt(sep.arg[10]);
|
|
|
|
npc_type->STR = 0;
|
|
npc_type->STA = 0;
|
|
npc_type->DEX = 0;
|
|
npc_type->AGI = 0;
|
|
npc_type->INT = 0;
|
|
npc_type->WIS = 0;
|
|
npc_type->CHA = 0;
|
|
|
|
npc_type->attack_delay = 3000;
|
|
|
|
npc_type->prim_melee_type = static_cast<uint8>(EQ::skills::SkillHandtoHand);
|
|
npc_type->sec_melee_type = static_cast<uint8>(EQ::skills::SkillHandtoHand);
|
|
|
|
auto npc = new NPC(npc_type, nullptr, position, GravityBehavior::Water);
|
|
npc->GiveNPCTypeData(npc_type);
|
|
|
|
entity_list.AddNPC(npc);
|
|
|
|
if (client) {
|
|
// Notify client of spawn data
|
|
client->Message(Chat::White, fmt::format("Name | {}", npc->name).c_str());
|
|
client->Message(Chat::White, fmt::format("Level | {}", npc->level).c_str());
|
|
client->Message(Chat::White, fmt::format("Health | {}", npc->max_hp).c_str());
|
|
client->Message(Chat::White, fmt::format("Race | {} ({})", GetRaceIDName(npc->race), npc->race).c_str());
|
|
client->Message(Chat::White, fmt::format("Class | {} ({})", GetClassIDName(npc->class_), npc->class_).c_str());
|
|
client->Message(Chat::White, fmt::format("Gender | {} ({})", GetGenderName(npc->gender), npc->gender).c_str());
|
|
client->Message(Chat::White, fmt::format("Texture | {}", npc->texture).c_str());
|
|
|
|
if (npc->d_melee_texture1 || npc->d_melee_texture2) {
|
|
client->Message(
|
|
Chat::White,
|
|
fmt::format(
|
|
"Weapon Item Number | Primary: {} Secondary: {}",
|
|
npc->d_melee_texture1,
|
|
npc->d_melee_texture2
|
|
).c_str()
|
|
);
|
|
}
|
|
|
|
if (npc->MerchantType) {
|
|
client->Message(Chat::White, fmt::format("Merchant ID | {}", npc->MerchantType).c_str());
|
|
}
|
|
|
|
if (npc->bodytype) {
|
|
client->Message(
|
|
Chat::White,
|
|
fmt::format(
|
|
"Body Type | {} ({})",
|
|
BodyType::GetName(npc->bodytype),
|
|
npc->bodytype
|
|
).c_str()
|
|
);
|
|
}
|
|
|
|
client->Message(Chat::White, "New NPC spawned!");
|
|
}
|
|
|
|
return npc;
|
|
}
|
|
}
|
|
|
|
uint32 ZoneDatabase::CreateNewNPCCommand(
|
|
const std::string& zone,
|
|
uint32 instance_version,
|
|
Client* c,
|
|
NPC* n,
|
|
uint32 extra
|
|
)
|
|
{
|
|
auto e = NpcTypesRepository::NewEntity();
|
|
|
|
e.id = NpcTypesRepository::GetOpenIDInZoneRange(*this, c->GetZoneID());
|
|
e.name = Strings::RemoveNumbers(n->GetName());
|
|
e.level = n->GetLevel();
|
|
e.race = n->GetRace();
|
|
e.class_ = n->GetClass();
|
|
e.hp = n->GetMaxHP();
|
|
e.mana = n->GetMaxMana();
|
|
e.gender = n->GetGender();
|
|
e.texture = n->GetTexture();
|
|
e.helmtexture = n->GetHelmTexture();
|
|
e.size = n->GetSize();
|
|
e.loottable_id = n->GetLoottableID();
|
|
e.merchant_id = n->MerchantType;
|
|
e.runspeed = n->GetRunspeed();
|
|
e.walkspeed = n->GetWalkspeed();
|
|
e.prim_melee_type = n->GetPrimSkill();
|
|
e.sec_melee_type = n->GetSecSkill();
|
|
|
|
e.bodytype = n->GetBodyType();
|
|
e.npc_faction_id = n->GetNPCFactionID();
|
|
e.aggroradius = n->GetAggroRange();
|
|
e.assistradius = n->GetAssistRange();
|
|
|
|
e.AC = n->GetAC();
|
|
e.ATK = n->GetATK();
|
|
e.STR = n->GetSTR();
|
|
e.STA = n->GetSTA();
|
|
e.AGI = n->GetAGI();
|
|
e.DEX = n->GetDEX();
|
|
e.WIS = n->GetWIS();
|
|
e._INT = n->GetINT();
|
|
e.CHA = n->GetCHA();
|
|
|
|
e.PR = n->GetPR();
|
|
e.MR = n->GetMR();
|
|
e.DR = n->GetDR();
|
|
e.FR = n->GetFR();
|
|
e.CR = n->GetCR();
|
|
e.Corrup = n->GetCorrup();
|
|
e.PhR = n->GetPhR();
|
|
|
|
e.Accuracy = n->GetAccuracyRating();
|
|
e.slow_mitigation = n->GetSlowMitigation();
|
|
e.mindmg = n->GetMinDMG();
|
|
e.maxdmg = n->GetMaxDMG();
|
|
e.hp_regen_rate = n->GetHPRegen();
|
|
e.hp_regen_per_second = n->GetHPRegenPerSecond();
|
|
//e.attack_delay = n->GetAttackDelay(); // Attack delay isn't copying correctly, 3000 becomes 18,400 in the copied NPC?
|
|
e.spellscale = n->GetSpellScale();
|
|
e.healscale = n->GetHealScale();
|
|
e.Avoidance = n->GetAvoidanceRating();
|
|
e.heroic_strikethrough = n->GetHeroicStrikethrough();
|
|
|
|
e.see_hide = n->SeeHide();
|
|
e.see_improved_hide = n->SeeImprovedHide();
|
|
e.see_invis = n->SeeInvisible();
|
|
e.see_invis_undead = n->SeeInvisibleUndead();
|
|
|
|
|
|
e = NpcTypesRepository::InsertOne(*this, e);
|
|
|
|
if (!e.id) {
|
|
return false;
|
|
}
|
|
|
|
auto sg = SpawngroupRepository::NewEntity();
|
|
|
|
sg.name = fmt::format(
|
|
"{}_{}_{}",
|
|
zone,
|
|
Strings::Escape(n->GetName()),
|
|
Timer::GetCurrentTime()
|
|
);
|
|
|
|
sg = SpawngroupRepository::InsertOne(*this, sg);
|
|
|
|
if (!sg.id) {
|
|
return false;
|
|
}
|
|
|
|
n->SetSpawnGroupId(sg.id);
|
|
n->SetNPCTypeID(e.id);
|
|
|
|
auto s2 = Spawn2Repository::NewEntity();
|
|
|
|
s2.zone = zone;
|
|
s2.version = instance_version;
|
|
s2.x = n->GetX();
|
|
s2.y = n->GetY();
|
|
s2.z = n->GetZ();
|
|
s2.respawntime = extra > 0 ? extra : 1200;
|
|
s2.heading = n->GetHeading();
|
|
s2.spawngroupID = sg.id;
|
|
|
|
s2 = Spawn2Repository::InsertOne(*this, s2);
|
|
|
|
if (!s2.id) {
|
|
return false;
|
|
}
|
|
|
|
auto se = SpawnentryRepository::NewEntity();
|
|
|
|
se.spawngroupID = sg.id;
|
|
se.npcID = e.id;
|
|
se.chance = 100;
|
|
|
|
se = SpawnentryRepository::InsertOne(*this, se);
|
|
|
|
if (!se.spawngroupID) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32 ZoneDatabase::AddNewNPCSpawnGroupCommand(
|
|
const std::string& zone,
|
|
uint32 instance_version,
|
|
Client* c,
|
|
NPC* n,
|
|
uint32 in_respawn_time
|
|
)
|
|
{
|
|
auto sg = SpawngroupRepository::InsertOne(
|
|
*this,
|
|
SpawngroupRepository::Spawngroup{
|
|
.name = fmt::format(
|
|
"{}_{}_{}",
|
|
zone,
|
|
Strings::Escape(n->GetName()),
|
|
Timer::GetCurrentTime()
|
|
)
|
|
}
|
|
);
|
|
|
|
if (!sg.id) {
|
|
return 0;
|
|
}
|
|
|
|
uint32 respawn_time = 1200;
|
|
uint32 spawn_id = 0;
|
|
|
|
if (in_respawn_time) {
|
|
respawn_time = in_respawn_time;
|
|
} else if (n->respawn2 && n->respawn2->RespawnTimer()) {
|
|
respawn_time = n->respawn2->RespawnTimer();
|
|
}
|
|
|
|
auto s2 = Spawn2Repository::NewEntity();
|
|
|
|
s2.zone = zone;
|
|
s2.version = instance_version;
|
|
s2.x = n->GetX();
|
|
s2.y = n->GetY();
|
|
s2.z = n->GetZ();
|
|
s2.heading = n->GetHeading();
|
|
s2.respawntime = respawn_time;
|
|
s2.spawngroupID = sg.id;
|
|
|
|
s2 = Spawn2Repository::InsertOne(*this, s2);
|
|
|
|
if (!s2.id) {
|
|
return 0;
|
|
}
|
|
|
|
auto se = SpawnentryRepository::NewEntity();
|
|
|
|
se.spawngroupID = sg.id;
|
|
se.npcID = n->GetNPCTypeID();
|
|
se.chance = 100;
|
|
|
|
se = SpawnentryRepository::InsertOne(*this, se);
|
|
|
|
if (!se.spawngroupID) {
|
|
return 0;
|
|
}
|
|
|
|
return s2.id;
|
|
}
|
|
|
|
uint32 ZoneDatabase::UpdateNPCTypeAppearance(Client* c, NPC* n)
|
|
{
|
|
auto e = NpcTypesRepository::FindOne(*this, n->GetNPCTypeID());
|
|
|
|
e.name = Strings::RemoveNumbers(n->GetName());
|
|
e.level = n->GetLevel();
|
|
e.race = n->GetRace();
|
|
e.class_ = n->GetClass();
|
|
e.hp = n->GetMaxHP();
|
|
e.gender = n->GetGender();
|
|
e.texture = n->GetTexture();
|
|
e.helmtexture = n->GetHelmTexture();
|
|
e.size = n->GetSize();
|
|
e.loottable_id = n->GetLoottableID();
|
|
e.merchant_id = n->MerchantType;
|
|
e.face = n->GetLuclinFace();
|
|
|
|
const int updated = NpcTypesRepository::UpdateOne(*this, e);
|
|
|
|
return updated;
|
|
}
|
|
|
|
uint32 ZoneDatabase::DeleteSpawnLeaveInNPCTypeTable(const std::string& zone, Client* c, NPC* n, uint32 remove_spawngroup_id)
|
|
{
|
|
if (!n->respawn2) {
|
|
return 0;
|
|
}
|
|
|
|
const auto& l = Spawn2Repository::GetWhere(
|
|
*this,
|
|
fmt::format(
|
|
"`id` = {} AND `zone` = '{}' AND `spawngroupID` = {}",
|
|
n->respawn2->GetID(),
|
|
zone,
|
|
n->GetSpawnGroupId()
|
|
)
|
|
);
|
|
|
|
if (l.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
auto e = l.front();
|
|
|
|
if (!Spawn2Repository::DeleteOne(*this, e.id)) {
|
|
return 0;
|
|
}
|
|
|
|
if (remove_spawngroup_id > 0) {
|
|
if (!SpawngroupRepository::DeleteOne(*this, e.spawngroupID)) {
|
|
return 0;
|
|
}
|
|
|
|
if (!SpawnentryRepository::DeleteOne(*this, e.spawngroupID)) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
uint32 ZoneDatabase::DeleteSpawnRemoveFromNPCTypeTable(
|
|
const std::string& zone,
|
|
uint32 instance_version,
|
|
Client* c,
|
|
NPC* n
|
|
)
|
|
{
|
|
const auto& l = Spawn2Repository::GetWhere(
|
|
*this,
|
|
fmt::format(
|
|
"`zone` = '{}' AND (`version` = {} OR `version` = -1) AND `spawngroupID` = {}",
|
|
zone,
|
|
instance_version,
|
|
n->GetSpawnGroupId()
|
|
)
|
|
);
|
|
|
|
if (l.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
auto e = l.front();
|
|
|
|
if (!Spawn2Repository::DeleteOne(*this, e.id)) {
|
|
return 0;
|
|
}
|
|
|
|
if (!SpawngroupRepository::DeleteOne(*this, e.spawngroupID)) {
|
|
return 0;
|
|
}
|
|
|
|
if (!SpawnentryRepository::DeleteOne(*this, e.spawngroupID)) {
|
|
return 0;
|
|
}
|
|
|
|
if (!NpcTypesRepository::DeleteOne(*this, n->GetNPCTypeID())) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
uint32 ZoneDatabase::AddSpawnFromSpawnGroup(
|
|
const std::string& zone,
|
|
uint32 instance_version,
|
|
Client* c,
|
|
NPC* n,
|
|
uint32 extra
|
|
)
|
|
{
|
|
auto e = Spawn2Repository::NewEntity();
|
|
|
|
e.zone = zone;
|
|
e.version = instance_version;
|
|
e.x = c->GetX();
|
|
e.y = c->GetY();
|
|
e.z = c->GetZ();
|
|
e.heading = c->GetHeading();
|
|
e.respawntime = extra > 0 ? extra : 1200;
|
|
e.spawngroupID = n->GetSpawnGroupId();
|
|
|
|
e = Spawn2Repository::InsertOne(*this, e);
|
|
|
|
if (!e.id) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
uint32 ZoneDatabase::AddNPCTypes(
|
|
const std::string& zone,
|
|
uint32 instance_version,
|
|
Client* c,
|
|
NPC* n,
|
|
uint32 spawngroup_id
|
|
)
|
|
{
|
|
auto e = NpcTypesRepository::NewEntity();
|
|
|
|
e.name = Strings::RemoveNumbers(n->GetName());
|
|
e.level = n->GetLevel();
|
|
e.race = n->GetRace();
|
|
e.class_ = n->GetClass();
|
|
e.hp = n->GetMaxHP();
|
|
e.gender = n->GetGender();
|
|
e.texture = n->GetTexture();
|
|
e.helmtexture = n->GetHelmTexture();
|
|
e.size = n->GetSize();
|
|
e.loottable_id = n->GetLoottableID();
|
|
e.merchant_id = n->MerchantType;
|
|
e.face = n->GetLuclinFace();
|
|
e.runspeed = n->GetRunspeed();
|
|
e.prim_melee_type = static_cast<uint8_t>(EQ::skills::SkillHandtoHand);
|
|
e.sec_melee_type = static_cast<uint8_t>(EQ::skills::SkillHandtoHand);
|
|
|
|
e = NpcTypesRepository::InsertOne(*this, e);
|
|
|
|
if (!e.id) {
|
|
return 0;
|
|
}
|
|
|
|
if (c) {
|
|
c->Message(
|
|
Chat::White,
|
|
fmt::format(
|
|
"NPC Created | ID: {} Name: {}",
|
|
e.id,
|
|
e.name
|
|
).c_str()
|
|
);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
uint32 ZoneDatabase::NPCSpawnDB(
|
|
uint8 command,
|
|
const std::string& zone,
|
|
uint32 instance_version,
|
|
Client *c,
|
|
NPC* n,
|
|
uint32 extra
|
|
)
|
|
{
|
|
switch (command) {
|
|
case NPCSpawnTypes::CreateNewSpawn: {
|
|
return CreateNewNPCCommand(zone, instance_version, c, n, extra);
|
|
}
|
|
case NPCSpawnTypes::AddNewSpawngroup: {
|
|
return AddNewNPCSpawnGroupCommand(zone, instance_version, c, n, extra);
|
|
}
|
|
case NPCSpawnTypes::UpdateAppearance: {
|
|
return UpdateNPCTypeAppearance(c, n);
|
|
}
|
|
case NPCSpawnTypes::RemoveSpawn: {
|
|
return DeleteSpawnLeaveInNPCTypeTable(zone, c, n, extra);
|
|
}
|
|
case NPCSpawnTypes::DeleteSpawn: {
|
|
return DeleteSpawnRemoveFromNPCTypeTable(zone, instance_version, c, n);
|
|
}
|
|
case NPCSpawnTypes::AddSpawnFromSpawngroup: {
|
|
return AddSpawnFromSpawnGroup(zone, instance_version, c, n, extra);
|
|
}
|
|
case NPCSpawnTypes::CreateNewNPC: {
|
|
return AddNPCTypes(zone, instance_version, c, n, extra);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uint32 NPC::GetEquipmentMaterial(uint8 material_slot) const
|
|
{
|
|
const uint32 texture_profile_material = GetTextureProfileMaterial(material_slot);
|
|
|
|
Log(Logs::Detail, Logs::MobAppearance, "[%s] material_slot: %u",
|
|
clean_name,
|
|
material_slot
|
|
);
|
|
|
|
if (texture_profile_material) {
|
|
return texture_profile_material;
|
|
}
|
|
|
|
if (material_slot >= EQ::textures::materialCount) {
|
|
return 0;
|
|
}
|
|
|
|
int16 invslot = EQ::InventoryProfile::CalcSlotFromMaterial(material_slot);
|
|
if (invslot == INVALID_INDEX) {
|
|
return 0;
|
|
}
|
|
|
|
if (equipment[invslot] == 0) {
|
|
switch (material_slot) {
|
|
case EQ::textures::armorHead:
|
|
return helmtexture;
|
|
case EQ::textures::armorChest:
|
|
return texture;
|
|
case EQ::textures::armorArms:
|
|
return armtexture;
|
|
case EQ::textures::armorWrist:
|
|
return bracertexture;
|
|
case EQ::textures::armorHands:
|
|
return handtexture;
|
|
case EQ::textures::armorLegs:
|
|
return legtexture;
|
|
case EQ::textures::armorFeet:
|
|
return feettexture;
|
|
case EQ::textures::weaponPrimary:
|
|
return d_melee_texture1;
|
|
case EQ::textures::weaponSecondary:
|
|
return d_melee_texture2;
|
|
default:
|
|
//they have nothing in the slot, and its not a special slot... they get nothing.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
//they have some loot item in this slot, pass it up to the default handler
|
|
return Mob::GetEquipmentMaterial(material_slot);
|
|
}
|
|
|
|
uint32 NPC::GetMaxDamage(uint8 tlevel)
|
|
{
|
|
uint64 dmg = 0;
|
|
if (tlevel < 40)
|
|
dmg = tlevel*2+2;
|
|
else if (tlevel < 50)
|
|
dmg = level*25/10+2;
|
|
else if (tlevel < 60)
|
|
dmg = (tlevel*3+2)+((tlevel-50)*30);
|
|
else
|
|
dmg = (tlevel*3+2)+((tlevel-50)*35);
|
|
return dmg;
|
|
}
|
|
|
|
void NPC::PickPocket(Client* thief)
|
|
{
|
|
thief->CheckIncreaseSkill(EQ::skills::SkillPickPockets, nullptr, 5);
|
|
|
|
//make sure were allowed to target them:
|
|
int over_level = GetLevel();
|
|
if(over_level > (thief->GetLevel() + THIEF_PICKPOCKET_OVER)) {
|
|
thief->Message(Chat::Red, "You are too inexperienced to pick pocket this target");
|
|
thief->SendPickPocketResponse(this, 0, PickPocketFailed);
|
|
//should we check aggro
|
|
return;
|
|
}
|
|
|
|
if(zone->random.Roll(5)) {
|
|
if (zone->CanDoCombat())
|
|
AddToHateList(thief, 50);
|
|
Say("Stop thief!");
|
|
thief->Message(Chat::Red, "You are noticed trying to steal!");
|
|
thief->SendPickPocketResponse(this, 0, PickPocketFailed);
|
|
return;
|
|
}
|
|
|
|
int steal_skill = thief->GetSkill(EQ::skills::SkillPickPockets);
|
|
int steal_chance = steal_skill * 100 / (5 * over_level + 5);
|
|
|
|
// Determine whether to steal money or an item.
|
|
uint32 money[6] = {0, ((steal_skill >= 125) ? (GetPlatinum()) : (0)), ((steal_skill >= 60) ? (GetGold()) : (0)), GetSilver(),
|
|
GetCopper(), 0 };
|
|
bool has_coin = ((money[PickPocketPlatinum] | money[PickPocketGold] | money[PickPocketSilver] | money[PickPocketCopper]) != 0);
|
|
bool steal_item = (steal_skill >= steal_chance && (zone->random.Roll(50) || !has_coin));
|
|
|
|
// still needs to have FindFreeSlot vs PutItemInInventory issue worked out
|
|
while (steal_item) {
|
|
std::vector<std::pair<const EQ::ItemData*, uint16>> loot_selection; // <const ItemData*, charges>
|
|
for (auto item_iter : m_loot_items) {
|
|
if (!item_iter || !item_iter->item_id)
|
|
continue;
|
|
|
|
auto item_test = database.GetItem(item_iter->item_id);
|
|
if (item_test->Magic || !item_test->NoDrop || item_test->IsClassBag() || thief->CheckLoreConflict(item_test) || item_iter->equip_slot != EQ::invslot::SLOT_INVALID)
|
|
continue;
|
|
|
|
loot_selection.emplace_back(std::make_pair(item_test, ((item_test->Stackable) ? (1) : (item_iter->charges))));
|
|
}
|
|
if (loot_selection.empty()) {
|
|
steal_item = false;
|
|
break;
|
|
}
|
|
|
|
int random = zone->random.Int(0, (loot_selection.size() - 1));
|
|
uint16 slot_id = thief->GetInv().FindFreeSlot(false, true, (loot_selection[random].first->Size), (loot_selection[random].first->ItemType == EQ::item::ItemTypeArrow));
|
|
if (slot_id == INVALID_INDEX) {
|
|
steal_item = false;
|
|
break;
|
|
}
|
|
|
|
auto item_inst = database.CreateItem(loot_selection[random].first, loot_selection[random].second);
|
|
if (item_inst == nullptr) {
|
|
steal_item = false;
|
|
break;
|
|
}
|
|
|
|
// Successful item pickpocket
|
|
if (item_inst->IsStackable() && RuleB(Character, UseStackablePickPocketing)) {
|
|
if (!thief->TryStacking(item_inst, ItemPacketTrade, false, false)) {
|
|
thief->PutItemInInventory(slot_id, *item_inst);
|
|
thief->SendItemPacket(slot_id, item_inst, ItemPacketTrade);
|
|
}
|
|
}
|
|
else {
|
|
thief->PutItemInInventory(slot_id, *item_inst);
|
|
thief->SendItemPacket(slot_id, item_inst, ItemPacketTrade);
|
|
}
|
|
RemoveItem(item_inst->GetID());
|
|
thief->SendPickPocketResponse(this, 0, PickPocketItem, item_inst->GetItem());
|
|
safe_delete(item_inst);
|
|
|
|
return;
|
|
}
|
|
|
|
while (!steal_item && has_coin) {
|
|
uint32 coin_amount = zone->random.Int(1, (steal_skill / 25) + 1);
|
|
|
|
int coin_type = PickPocketPlatinum;
|
|
while (coin_type <= PickPocketCopper) {
|
|
if (money[coin_type]) {
|
|
if (coin_amount > money[coin_type])
|
|
coin_amount = money[coin_type];
|
|
break;
|
|
}
|
|
++coin_type;
|
|
}
|
|
if (coin_type > PickPocketCopper)
|
|
break;
|
|
|
|
memset(money, 0, (sizeof(int) * 6));
|
|
money[coin_type] = coin_amount;
|
|
|
|
if (zone->random.Roll(steal_chance)) { // Successful coin pickpocket
|
|
switch (coin_type) {
|
|
case PickPocketPlatinum:
|
|
SetPlatinum(GetPlatinum() - coin_amount);
|
|
break;
|
|
case PickPocketGold:
|
|
SetGold(GetGold() - coin_amount);
|
|
break;
|
|
case PickPocketSilver:
|
|
SetSilver(GetSilver() - coin_amount);
|
|
break;
|
|
case PickPocketCopper:
|
|
SetCopper(GetCopper() - coin_amount);
|
|
break;
|
|
default: // has_coin..but, doesn't have coin?
|
|
thief->SendPickPocketResponse(this, 0, PickPocketFailed);
|
|
return;
|
|
}
|
|
|
|
thief->AddMoneyToPP(money[PickPocketCopper], money[PickPocketSilver], money[PickPocketGold], money[PickPocketPlatinum], false);
|
|
thief->SendPickPocketResponse(this, coin_amount, coin_type);
|
|
return;
|
|
}
|
|
|
|
thief->SendPickPocketResponse(this, 0, PickPocketFailed);
|
|
return;
|
|
}
|
|
|
|
thief->Message(Chat::White, "This target's pockets are empty");
|
|
thief->SendPickPocketResponse(this, 0, PickPocketFailed);
|
|
}
|
|
|
|
void NPC::Disarm(Client* client, int chance) {
|
|
// disarm primary if available, otherwise disarm secondary
|
|
const EQ::ItemData* weapon = NULL;
|
|
uint8 eslot = 0xFF;
|
|
if (equipment[EQ::invslot::slotPrimary] != 0)
|
|
eslot = EQ::invslot::slotPrimary;
|
|
else if (equipment[EQ::invslot::slotSecondary] != 0)
|
|
eslot = EQ::invslot::slotSecondary;
|
|
if (eslot != 0xFF) {
|
|
if (zone->random.Int(0, 1000) <= chance) {
|
|
weapon = database.GetItem(equipment[eslot]);
|
|
if (weapon) {
|
|
if (!weapon->Magic && weapon->NoDrop != 0) {
|
|
int16 charges = -1;
|
|
LootItems::iterator cur, end;
|
|
cur = m_loot_items.begin();
|
|
end = m_loot_items.end();
|
|
// Get charges for the item in the loot table
|
|
for (; cur != end; cur++) {
|
|
LootItem * citem = *cur;
|
|
if (citem->item_id == weapon->ID) {
|
|
charges = citem->charges;
|
|
break;
|
|
}
|
|
}
|
|
EQ::ItemInstance *inst = NULL;
|
|
inst = database.CreateItem(weapon->ID, charges);
|
|
// Remove item from loot table
|
|
RemoveItem(weapon->ID);
|
|
CalcBonuses();
|
|
if (inst) {
|
|
// create a ground item
|
|
Object* object = new Object(inst, GetX(), GetY(), GetZ(), 0.0f, 300000);
|
|
entity_list.AddObject(object, true);
|
|
object->StartDecay();
|
|
safe_delete(inst);
|
|
}
|
|
}
|
|
}
|
|
// Update Appearance
|
|
equipment[eslot] = 0;
|
|
int matslot = eslot == EQ::invslot::slotPrimary ? EQ::textures::weaponPrimary : EQ::textures::weaponSecondary;
|
|
if (matslot != -1)
|
|
SendWearChange(matslot);
|
|
if ((CastToMob()->GetBodyType() == BodyType::Humanoid || CastToMob()->GetBodyType() == BodyType::Summoned) && eslot == EQ::invslot::slotPrimary)
|
|
Say("Ahh! My weapon!");
|
|
client->MessageString(Chat::Skills, DISARM_SUCCESS, GetCleanName());
|
|
if (chance != 1000)
|
|
client->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 4);
|
|
return;
|
|
}
|
|
client->MessageString(Chat::Skills, DISARM_FAILED);
|
|
if (chance != 1000)
|
|
client->CheckIncreaseSkill(EQ::skills::SkillDisarm, nullptr, 2);
|
|
return;
|
|
}
|
|
client->MessageString(Chat::Skills, DISARM_FAILED);
|
|
}
|
|
|
|
void Mob::NPCSpecialAttacks(const char* parse, int permtag, bool reset, bool remove) {
|
|
if(reset)
|
|
{
|
|
ClearSpecialAbilities();
|
|
}
|
|
|
|
const char* orig_parse = parse;
|
|
while (*parse)
|
|
{
|
|
switch(*parse)
|
|
{
|
|
case 'E':
|
|
SetSpecialAbility(SpecialAbility::Enrage, remove ? 0 : 1);
|
|
break;
|
|
case 'F':
|
|
SetSpecialAbility(SpecialAbility::Flurry, remove ? 0 : 1);
|
|
break;
|
|
case 'R':
|
|
SetSpecialAbility(SpecialAbility::Rampage, remove ? 0 : 1);
|
|
break;
|
|
case 'r':
|
|
SetSpecialAbility(SpecialAbility::AreaRampage, remove ? 0 : 1);
|
|
break;
|
|
case 'S':
|
|
if(remove) {
|
|
SetSpecialAbility(SpecialAbility::Summon, 0);
|
|
StopSpecialAbilityTimer(SpecialAbility::Summon);
|
|
} else {
|
|
SetSpecialAbility(SpecialAbility::Summon, 1);
|
|
}
|
|
break;
|
|
case 'T':
|
|
SetSpecialAbility(SpecialAbility::TripleAttack, remove ? 0 : 1);
|
|
break;
|
|
case 'Q':
|
|
//quad requires triple to work properly
|
|
if(remove) {
|
|
SetSpecialAbility(SpecialAbility::QuadrupleAttack, 0);
|
|
} else {
|
|
SetSpecialAbility(SpecialAbility::TripleAttack, 1);
|
|
SetSpecialAbility(SpecialAbility::QuadrupleAttack, 1);
|
|
}
|
|
break;
|
|
case 'b':
|
|
SetSpecialAbility(SpecialAbility::BaneAttack, remove ? 0 : 1);
|
|
break;
|
|
case 'm':
|
|
SetSpecialAbility(SpecialAbility::MagicalAttack, remove ? 0 : 1);
|
|
break;
|
|
case 'U':
|
|
SetSpecialAbility(SpecialAbility::SlowImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'M':
|
|
SetSpecialAbility(SpecialAbility::MesmerizeImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'C':
|
|
SetSpecialAbility(SpecialAbility::CharmImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'N':
|
|
SetSpecialAbility(SpecialAbility::StunImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'I':
|
|
SetSpecialAbility(SpecialAbility::SnareImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'D':
|
|
SetSpecialAbility(SpecialAbility::FearImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'K':
|
|
SetSpecialAbility(SpecialAbility::DispellImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'A':
|
|
SetSpecialAbility(SpecialAbility::MeleeImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'B':
|
|
SetSpecialAbility(SpecialAbility::MagicImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'f':
|
|
SetSpecialAbility(SpecialAbility::FleeingImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'O':
|
|
SetSpecialAbility(SpecialAbility::MeleeImmunityExceptBane, remove ? 0 : 1);
|
|
break;
|
|
case 'W':
|
|
SetSpecialAbility(SpecialAbility::MeleeImmunityExceptMagical, remove ? 0 : 1);
|
|
break;
|
|
case 'H':
|
|
SetSpecialAbility(SpecialAbility::AggroImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'G':
|
|
SetSpecialAbility(SpecialAbility::BeingAggroImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'g':
|
|
SetSpecialAbility(SpecialAbility::CastingFromRangeImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'd':
|
|
SetSpecialAbility(SpecialAbility::FeignDeathImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'Y':
|
|
SetSpecialAbility(SpecialAbility::RangedAttack, remove ? 0 : 1);
|
|
break;
|
|
case 'L':
|
|
SetSpecialAbility(SpecialAbility::DualWield, remove ? 0 : 1);
|
|
break;
|
|
case 't':
|
|
SetSpecialAbility(SpecialAbility::TunnelVision, remove ? 0 : 1);
|
|
break;
|
|
case 'n':
|
|
SetSpecialAbility(SpecialAbility::NoBuffHealFriends, remove ? 0 : 1);
|
|
break;
|
|
case 'p':
|
|
SetSpecialAbility(SpecialAbility::PacifyImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'J':
|
|
SetSpecialAbility(SpecialAbility::Leash, remove ? 0 : 1);
|
|
break;
|
|
case 'j':
|
|
SetSpecialAbility(SpecialAbility::Tether, remove ? 0 : 1);
|
|
break;
|
|
case 'o':
|
|
SetSpecialAbility(SpecialAbility::DestructibleObject, remove ? 0 : 1);
|
|
SetDestructibleObject(remove ? true : false);
|
|
break;
|
|
case 'Z':
|
|
SetSpecialAbility(SpecialAbility::HarmFromClientImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'i':
|
|
SetSpecialAbility(SpecialAbility::TauntImmunity, remove ? 0 : 1);
|
|
break;
|
|
case 'e':
|
|
SetSpecialAbility(SpecialAbility::AlwaysFlee, remove ? 0 : 1);
|
|
break;
|
|
case 'h':
|
|
SetSpecialAbility(SpecialAbility::FleePercent, remove ? 0 : 1);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
parse++;
|
|
}
|
|
|
|
if(permtag == 1 && GetNPCTypeID() > 0)
|
|
{
|
|
if(content_db.SetSpecialAttkFlag(GetNPCTypeID(), orig_parse))
|
|
{
|
|
LogInfo("NPCTypeID: [{}] flagged to [{}] for Special Attacks.\n",GetNPCTypeID(),orig_parse);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Mob::HasNPCSpecialAtk(const char* parse) {
|
|
|
|
bool HasAllAttacks = true;
|
|
|
|
while (*parse && HasAllAttacks == true)
|
|
{
|
|
switch(*parse)
|
|
{
|
|
case 'E':
|
|
if (!GetSpecialAbility(SpecialAbility::Enrage))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'F':
|
|
if (!GetSpecialAbility(SpecialAbility::Flurry))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'R':
|
|
if (!GetSpecialAbility(SpecialAbility::Rampage))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'r':
|
|
if (!GetSpecialAbility(SpecialAbility::AreaRampage))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'S':
|
|
if (!GetSpecialAbility(SpecialAbility::Summon))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'T':
|
|
if (!GetSpecialAbility(SpecialAbility::TripleAttack))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'Q':
|
|
if (!GetSpecialAbility(SpecialAbility::QuadrupleAttack))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'b':
|
|
if (!GetSpecialAbility(SpecialAbility::BaneAttack))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'm':
|
|
if (!GetSpecialAbility(SpecialAbility::MagicalAttack))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'U':
|
|
if (!GetSpecialAbility(SpecialAbility::SlowImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'M':
|
|
if (!GetSpecialAbility(SpecialAbility::MesmerizeImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'C':
|
|
if (!GetSpecialAbility(SpecialAbility::CharmImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'N':
|
|
if (!GetSpecialAbility(SpecialAbility::StunImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'I':
|
|
if (!GetSpecialAbility(SpecialAbility::SnareImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'D':
|
|
if (!GetSpecialAbility(SpecialAbility::FearImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'A':
|
|
if (!GetSpecialAbility(SpecialAbility::MeleeImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'B':
|
|
if (!GetSpecialAbility(SpecialAbility::MagicImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'f':
|
|
if (!GetSpecialAbility(SpecialAbility::FleeingImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'O':
|
|
if (!GetSpecialAbility(SpecialAbility::MeleeImmunityExceptBane))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'W':
|
|
if (!GetSpecialAbility(SpecialAbility::MeleeImmunityExceptMagical))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'H':
|
|
if (!GetSpecialAbility(SpecialAbility::AggroImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'G':
|
|
if (!GetSpecialAbility(SpecialAbility::BeingAggroImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'g':
|
|
if (!GetSpecialAbility(SpecialAbility::CastingFromRangeImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'd':
|
|
if (!GetSpecialAbility(SpecialAbility::FeignDeathImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'Y':
|
|
if (!GetSpecialAbility(SpecialAbility::RangedAttack))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'L':
|
|
if (!GetSpecialAbility(SpecialAbility::DualWield))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 't':
|
|
if (!GetSpecialAbility(SpecialAbility::TunnelVision))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'n':
|
|
if (!GetSpecialAbility(SpecialAbility::NoBuffHealFriends))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'p':
|
|
if(!GetSpecialAbility(SpecialAbility::PacifyImmunity))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'J':
|
|
if(!GetSpecialAbility(SpecialAbility::Leash))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'j':
|
|
if(!GetSpecialAbility(SpecialAbility::Tether))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'o':
|
|
if(!GetSpecialAbility(SpecialAbility::DestructibleObject))
|
|
{
|
|
HasAllAttacks = false;
|
|
SetDestructibleObject(false);
|
|
}
|
|
break;
|
|
case 'Z':
|
|
if(!GetSpecialAbility(SpecialAbility::HarmFromClientImmunity)){
|
|
HasAllAttacks = false;
|
|
}
|
|
break;
|
|
case 'e':
|
|
if(!GetSpecialAbility(SpecialAbility::AlwaysFlee))
|
|
HasAllAttacks = false;
|
|
break;
|
|
case 'h':
|
|
if(!GetSpecialAbility(SpecialAbility::FleePercent))
|
|
HasAllAttacks = false;
|
|
break;
|
|
default:
|
|
HasAllAttacks = false;
|
|
break;
|
|
}
|
|
parse++;
|
|
}
|
|
|
|
return HasAllAttacks;
|
|
}
|
|
|
|
void NPC::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho)
|
|
{
|
|
Mob::FillSpawnStruct(ns, ForWho);
|
|
PetOnSpawn(ns);
|
|
ns->spawn.is_npc = 1;
|
|
UpdateActiveLight();
|
|
ns->spawn.light = GetActiveLightType();
|
|
ns->spawn.show_name = NPCTypedata->show_name;
|
|
ns->spawn.trader = false;
|
|
}
|
|
|
|
void NPC::PetOnSpawn(NewSpawn_Struct* ns)
|
|
{
|
|
//Basic settings to make sure swarm pets work properly.
|
|
Mob *swarm_owner = nullptr;
|
|
if (GetSwarmOwner()) {
|
|
swarm_owner = entity_list.GetMobID(GetSwarmOwner());
|
|
}
|
|
|
|
if (swarm_owner) {
|
|
if (swarm_owner->IsClient()) {
|
|
SetPetOwnerClient(true); //Simple flag to determine if pet belongs to a client
|
|
SetAllowBeneficial(true);//Allow temp pets to receive buffs and heals if owner is client.
|
|
//This will allow CLIENT swarm pets NOT to be targeted with F8.
|
|
ns->spawn.targetable_with_hotkey = false;
|
|
no_target_hotkey = true;
|
|
} else {
|
|
//NPC cast swarm pets should still be targetable with F8.
|
|
ns->spawn.targetable_with_hotkey = true;
|
|
no_target_hotkey = false;
|
|
}
|
|
|
|
SetTempPet(true); //Simple mob flag for checking if temp pet
|
|
swarm_owner->SetTempPetsActive(true); //Necessary fail safe flag set if mob ever had a swarm pet to ensure they are removed.
|
|
swarm_owner->SetTempPetCount(swarm_owner->GetTempPetCount() + 1);
|
|
|
|
//Not recommended if using above (However, this will work better on older clients).
|
|
if (RuleB(Pets, UnTargetableSwarmPet)) {
|
|
ns->spawn.bodytype = BodyType::NoTarget;
|
|
}
|
|
|
|
if (
|
|
!IsCharmed() &&
|
|
swarm_owner->IsClient() &&
|
|
RuleB(Pets, ClientPetsUseOwnerNameInLastName)
|
|
) {
|
|
const auto& tmp_lastname = fmt::format("{}'s Pet", swarm_owner->GetName());
|
|
if (tmp_lastname.size() < sizeof(ns->spawn.lastName)) {
|
|
strn0cpy(ns->spawn.lastName, tmp_lastname.c_str(), sizeof(ns->spawn.lastName));
|
|
}
|
|
}
|
|
|
|
if (swarm_owner->IsBot()) {
|
|
SetPetOwnerBot(true);
|
|
} else if (swarm_owner->IsNPC()) {
|
|
SetPetOwnerNPC(true);
|
|
}
|
|
} else if (GetOwnerID()) {
|
|
ns->spawn.is_pet = 1;
|
|
|
|
if (!IsCharmed()) {
|
|
const auto c = entity_list.GetClientByID(GetOwnerID());
|
|
if (c) {
|
|
SetPetOwnerClient(true);
|
|
if (RuleB(Pets, ClientPetsUseOwnerNameInLastName)) {
|
|
const auto& tmp_lastname = fmt::format("{}'s Pet", c->GetName());
|
|
if (tmp_lastname.size() < sizeof(ns->spawn.lastName)) {
|
|
strn0cpy(ns->spawn.lastName, tmp_lastname.c_str(), sizeof(ns->spawn.lastName));
|
|
}
|
|
}
|
|
} else {
|
|
Mob* owner = entity_list.GetMob(GetOwnerID());
|
|
if (owner) {
|
|
if (owner->IsBot()) {
|
|
SetPetOwnerBot(true);
|
|
} else if (owner->IsNPC()) {
|
|
SetPetOwnerNPC(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ns->spawn.is_pet = 0;
|
|
}
|
|
}
|
|
|
|
void NPC::SetLevel(uint8 in_level, bool command)
|
|
{
|
|
if(in_level > level)
|
|
SendLevelAppearance();
|
|
level = in_level;
|
|
SendAppearancePacket(AppearanceType::WhoLevel, in_level);
|
|
}
|
|
|
|
void NPC::ModifyNPCStat(const std::string& stat, const std::string& value)
|
|
{
|
|
auto stat_lower = Strings::ToLower(stat);
|
|
|
|
auto variable_key = fmt::format(
|
|
"modify_stat_{}",
|
|
stat_lower
|
|
);
|
|
|
|
SetEntityVariable(variable_key, value);
|
|
|
|
LogNPCScaling("NPC::ModifyNPCStat: Key [{}] Value [{}] ", variable_key, value);
|
|
|
|
if (stat_lower == "ac") {
|
|
AC = Strings::ToInt(value);
|
|
CalcAC();
|
|
return;
|
|
}
|
|
else if (stat_lower == "str") {
|
|
STR = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "sta") {
|
|
STA = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "agi") {
|
|
AGI = Strings::ToInt(value);
|
|
CalcAC();
|
|
return;
|
|
}
|
|
else if (stat_lower == "dex") {
|
|
DEX = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "wis") {
|
|
WIS = Strings::ToInt(value);
|
|
CalcMaxMana();
|
|
return;
|
|
}
|
|
else if (stat_lower == "int" || stat_lower == "_int") {
|
|
INT = Strings::ToInt(value);
|
|
CalcMaxMana();
|
|
return;
|
|
}
|
|
else if (stat_lower == "cha") {
|
|
CHA = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "max_hp") {
|
|
base_hp = Strings::ToBigInt(value);
|
|
|
|
CalcMaxHP();
|
|
if (current_hp > max_hp) {
|
|
current_hp = max_hp;
|
|
}
|
|
|
|
return;
|
|
}
|
|
else if (stat_lower == "max_mana") {
|
|
npc_mana = Strings::ToUnsignedBigInt(value);
|
|
CalcMaxMana();
|
|
if (current_mana > max_mana) {
|
|
current_mana = max_mana;
|
|
}
|
|
return;
|
|
}
|
|
else if (stat_lower == "mr") {
|
|
MR = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "fr") {
|
|
FR = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "cr") {
|
|
CR = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "cor") {
|
|
Corrup = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "pr") {
|
|
PR = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "dr") {
|
|
DR = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "phr") {
|
|
PhR = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "runspeed") {
|
|
runspeed = Strings::ToFloat(value);
|
|
base_runspeed = (int) (runspeed * 40.0f);
|
|
base_walkspeed = base_runspeed * 100 / 265;
|
|
walkspeed = ((float) base_walkspeed) * 0.025f;
|
|
base_fearspeed = base_runspeed * 100 / 127;
|
|
fearspeed = ((float) base_fearspeed) * 0.025f;
|
|
CalcBonuses();
|
|
return;
|
|
}
|
|
else if (stat_lower == "special_attacks") {
|
|
NPCSpecialAttacks(value.c_str(), 0, true);
|
|
return;
|
|
}
|
|
else if (stat_lower == "special_abilities") {
|
|
ProcessSpecialAbilities(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "attack_speed") {
|
|
attack_speed = Strings::ToFloat(value);
|
|
CalcBonuses();
|
|
return;
|
|
}
|
|
else if (stat_lower == "attack_delay") {
|
|
/* TODO: fix DB */
|
|
attack_delay = Strings::ToInt(value) * 100;
|
|
CalcBonuses();
|
|
return;
|
|
}
|
|
else if (stat_lower == "atk") {
|
|
ATK = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "accuracy") {
|
|
accuracy_rating = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "avoidance") {
|
|
avoidance_rating = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "trackable") {
|
|
trackable = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "min_hit") {
|
|
min_dmg = Strings::ToInt(value);
|
|
// Clamp max_dmg to be >= min_dmg
|
|
max_dmg = std::max(min_dmg, max_dmg);
|
|
base_damage = round((max_dmg - min_dmg) / 1.9);
|
|
min_damage = min_dmg - round(base_damage / 10.0);
|
|
return;
|
|
}
|
|
else if (stat_lower == "max_hit") {
|
|
max_dmg = Strings::ToInt(value);
|
|
// Clamp min_dmg to be <= max_dmg
|
|
min_dmg = std::min(min_dmg, max_dmg);
|
|
base_damage = round((max_dmg - min_dmg) / 1.9);
|
|
min_damage = min_dmg - round(base_damage / 10.0);
|
|
return;
|
|
}
|
|
else if (stat_lower == "attack_count") {
|
|
attack_count = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "see_invis") {
|
|
see_invis = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "see_invis_undead") {
|
|
see_invis_undead = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "see_hide") {
|
|
see_hide = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "see_improved_hide") {
|
|
see_improved_hide = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "hp_regen") {
|
|
hp_regen = Strings::ToBigInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "hp_regen_per_second") {
|
|
hp_regen_per_second = Strings::ToBigInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "mana_regen") {
|
|
mana_regen = Strings::ToBigInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "level") {
|
|
SetLevel(Strings::ToInt(value));
|
|
return;
|
|
}
|
|
else if (stat_lower == "aggro") {
|
|
pAggroRange = Strings::ToFloat(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "assist") {
|
|
pAssistRange = Strings::ToFloat(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "slow_mitigation") {
|
|
slow_mitigation = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "loottable_id") {
|
|
m_loottable_id = Strings::ToFloat(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "healscale") {
|
|
healscale = Strings::ToFloat(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "spellscale") {
|
|
spellscale = Strings::ToFloat(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "npc_spells_id") {
|
|
AI_AddNPCSpells(Strings::ToInt(value));
|
|
return;
|
|
}
|
|
else if (stat_lower == "npc_spells_effects_id") {
|
|
AI_AddNPCSpellsEffects(Strings::ToInt(value));
|
|
CalcBonuses();
|
|
return;
|
|
}
|
|
else if (stat_lower == "heroic_strikethrough") {
|
|
heroic_strikethrough = Strings::ToInt(value);
|
|
return;
|
|
}
|
|
else if (stat_lower == "keeps_sold_items") {
|
|
SetKeepsSoldItems(Strings::ToBool(value));
|
|
return;
|
|
}
|
|
}
|
|
|
|
float NPC::GetNPCStat(const std::string& stat)
|
|
{
|
|
|
|
if (auto stat_lower = Strings::ToLower(stat); stat_lower == "ac") {
|
|
return AC;
|
|
}
|
|
else if (stat_lower == "str") {
|
|
return STR;
|
|
}
|
|
else if (stat_lower == "sta") {
|
|
return STA;
|
|
}
|
|
else if (stat_lower == "agi") {
|
|
return AGI;
|
|
}
|
|
else if (stat_lower == "dex") {
|
|
return DEX;
|
|
}
|
|
else if (stat_lower == "wis") {
|
|
return WIS;
|
|
}
|
|
else if (stat_lower == "int" || stat_lower == "_int") {
|
|
return INT;
|
|
}
|
|
else if (stat_lower == "cha") {
|
|
return CHA;
|
|
}
|
|
else if (stat_lower == "max_hp") {
|
|
return base_hp;
|
|
}
|
|
else if (stat_lower == "max_mana") {
|
|
return npc_mana;
|
|
}
|
|
else if (stat_lower == "mr") {
|
|
return MR;
|
|
}
|
|
else if (stat_lower == "fr") {
|
|
return FR;
|
|
}
|
|
else if (stat_lower == "cr") {
|
|
return CR;
|
|
}
|
|
else if (stat_lower == "cor") {
|
|
return Corrup;
|
|
}
|
|
else if (stat_lower == "phr") {
|
|
return PhR;
|
|
}
|
|
else if (stat_lower == "pr") {
|
|
return PR;
|
|
}
|
|
else if (stat_lower == "dr") {
|
|
return DR;
|
|
}
|
|
else if (stat_lower == "runspeed") {
|
|
return runspeed;
|
|
}
|
|
else if (stat_lower == "attack_speed") {
|
|
return attack_speed;
|
|
}
|
|
else if (stat_lower == "attack_delay") {
|
|
return attack_delay;
|
|
}
|
|
else if (stat_lower == "atk") {
|
|
return ATK;
|
|
}
|
|
else if (stat_lower == "accuracy") {
|
|
return accuracy_rating;
|
|
}
|
|
else if (stat_lower == "avoidance") {
|
|
return avoidance_rating;
|
|
}
|
|
else if (stat_lower == "trackable") {
|
|
return trackable;
|
|
}
|
|
else if (stat_lower == "min_hit") {
|
|
return min_dmg;
|
|
}
|
|
else if (stat_lower == "max_hit") {
|
|
return max_dmg;
|
|
}
|
|
else if (stat_lower == "attack_count") {
|
|
return attack_count;
|
|
}
|
|
else if (stat_lower == "see_invis") {
|
|
return see_invis;
|
|
}
|
|
else if (stat_lower == "see_invis_undead") {
|
|
return see_invis_undead;
|
|
}
|
|
else if (stat_lower == "see_hide") {
|
|
return see_hide;
|
|
}
|
|
else if (stat_lower == "see_improved_hide") {
|
|
return see_improved_hide;
|
|
}
|
|
else if (stat_lower == "hp_regen") {
|
|
return hp_regen;
|
|
}
|
|
else if (stat_lower == "hp_regen_per_second") {
|
|
return hp_regen_per_second;
|
|
}
|
|
else if (stat_lower == "mana_regen") {
|
|
return mana_regen;
|
|
}
|
|
else if (stat_lower == "level") {
|
|
return GetOrigLevel();
|
|
}
|
|
else if (stat_lower == "aggro") {
|
|
return pAggroRange;
|
|
}
|
|
else if (stat_lower == "assist") {
|
|
return pAssistRange;
|
|
}
|
|
else if (stat_lower == "slow_mitigation") {
|
|
return slow_mitigation;
|
|
}
|
|
else if (stat_lower == "loottable_id") {
|
|
return m_loottable_id;
|
|
}
|
|
else if (stat_lower == "healscale") {
|
|
return healscale;
|
|
}
|
|
else if (stat_lower == "spellscale") {
|
|
return spellscale;
|
|
}
|
|
else if (stat_lower == "npc_spells_id") {
|
|
return npc_spells_id;
|
|
}
|
|
else if (stat_lower == "npc_spells_effects_id") {
|
|
return npc_spells_effects_id;
|
|
}
|
|
else if (stat_lower == "heroic_strikethrough") {
|
|
return heroic_strikethrough;
|
|
}
|
|
else if (stat_lower == "keeps_sold_items") {
|
|
return keeps_sold_items;
|
|
}
|
|
//default values
|
|
else if (stat_lower == "default_ac") {
|
|
return default_ac;
|
|
}
|
|
else if (stat_lower == "default_min_hit") {
|
|
return default_min_dmg;
|
|
}
|
|
else if (stat_lower == "default_max_hit") {
|
|
return default_max_dmg;
|
|
}
|
|
else if (stat_lower == "default_attack_delay") {
|
|
return default_attack_delay;
|
|
}
|
|
else if (stat_lower == "default_accuracy") {
|
|
return default_accuracy_rating;
|
|
}
|
|
else if (stat_lower == "default_avoidance") {
|
|
return default_avoidance_rating;
|
|
}
|
|
else if (stat_lower == "default_atk") {
|
|
return default_atk;
|
|
}
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
void NPC::LevelScale() {
|
|
|
|
uint8 random_level = (zone->random.Int(level, maxlevel));
|
|
|
|
float scaling = (((random_level / (float)level) - 1) * (scalerate / 100.0f));
|
|
|
|
if (RuleB(NPC, NewLevelScaling)) {
|
|
if (scalerate == 0 || maxlevel <= 25) {
|
|
// Don't add HP to dynamically scaled NPCs since this will be calculated later
|
|
if (max_hp > 0 || m_skip_auto_scale)
|
|
{
|
|
// pre-pop seems to scale by 20 HP increments while newer by 100
|
|
// We also don't want 100 increments on newer noobie zones, check level
|
|
if (zone->GetZoneID() < 200 || level < 48) {
|
|
max_hp += (random_level - level) * 20;
|
|
base_hp += (random_level - level) * 20;
|
|
} else {
|
|
max_hp += (random_level - level) * 100;
|
|
base_hp += (random_level - level) * 100;
|
|
}
|
|
|
|
current_hp = max_hp;
|
|
}
|
|
|
|
// Don't add max_dmg to dynamically scaled NPCs since this will be calculated later
|
|
if (max_dmg > 0 || m_skip_auto_scale)
|
|
{
|
|
max_dmg += (random_level - level) * 2;
|
|
}
|
|
} else {
|
|
uint8 scale_adjust = 1;
|
|
|
|
base_hp += (int64)(base_hp * scaling);
|
|
max_hp += (int64)(max_hp * scaling);
|
|
current_hp = max_hp;
|
|
|
|
if (max_dmg) {
|
|
max_dmg += (int64)(max_dmg * scaling / scale_adjust);
|
|
min_dmg += (int64)(min_dmg * scaling / scale_adjust);
|
|
}
|
|
|
|
STR += (int)(STR * scaling / scale_adjust);
|
|
STA += (int)(STA * scaling / scale_adjust);
|
|
AGI += (int)(AGI * scaling / scale_adjust);
|
|
DEX += (int)(DEX * scaling / scale_adjust);
|
|
INT += (int)(INT * scaling / scale_adjust);
|
|
WIS += (int)(WIS * scaling / scale_adjust);
|
|
CHA += (int)(CHA * scaling / scale_adjust);
|
|
if (MR)
|
|
MR += (int)(MR * scaling / scale_adjust);
|
|
if (CR)
|
|
CR += (int)(CR * scaling / scale_adjust);
|
|
if (DR)
|
|
DR += (int)(DR * scaling / scale_adjust);
|
|
if (FR)
|
|
FR += (int)(FR * scaling / scale_adjust);
|
|
if (PR)
|
|
PR += (int)(PR * scaling / scale_adjust);
|
|
}
|
|
} else {
|
|
// Compensate for scale rates at low levels so they don't add too much
|
|
uint8 scale_adjust = 1;
|
|
if(level > 0 && level <= 5)
|
|
scale_adjust = 10;
|
|
if(level > 5 && level <= 10)
|
|
scale_adjust = 5;
|
|
if(level > 10 && level <= 15)
|
|
scale_adjust = 3;
|
|
if(level > 15 && level <= 25)
|
|
scale_adjust = 2;
|
|
|
|
AC += (int)(AC * scaling);
|
|
ATK += (int)(ATK * scaling);
|
|
base_hp += (int64)(base_hp * scaling);
|
|
max_hp += (int64)(max_hp * scaling);
|
|
current_hp = max_hp;
|
|
STR += (int)(STR * scaling / scale_adjust);
|
|
STA += (int)(STA * scaling / scale_adjust);
|
|
AGI += (int)(AGI * scaling / scale_adjust);
|
|
DEX += (int)(DEX * scaling / scale_adjust);
|
|
INT += (int)(INT * scaling / scale_adjust);
|
|
WIS += (int)(WIS * scaling / scale_adjust);
|
|
CHA += (int)(CHA * scaling / scale_adjust);
|
|
if (MR)
|
|
MR += (int)(MR * scaling / scale_adjust);
|
|
if (CR)
|
|
CR += (int)(CR * scaling / scale_adjust);
|
|
if (DR)
|
|
DR += (int)(DR * scaling / scale_adjust);
|
|
if (FR)
|
|
FR += (int)(FR * scaling / scale_adjust);
|
|
if (PR)
|
|
PR += (int)(PR * scaling / scale_adjust);
|
|
|
|
if (max_dmg)
|
|
{
|
|
max_dmg += (int64)(max_dmg * scaling / scale_adjust);
|
|
min_dmg += (int64)(min_dmg * scaling / scale_adjust);
|
|
}
|
|
|
|
}
|
|
level = random_level;
|
|
|
|
}
|
|
|
|
uint32 NPC::GetSpawnPointID() const
|
|
{
|
|
if (respawn2) {
|
|
return respawn2->GetID();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void NPC::NPCSlotTexture(uint8 slot, uint32 texture)
|
|
{
|
|
if (slot == EQ::textures::TextureSlot::weaponPrimary) {
|
|
d_melee_texture1 = texture;
|
|
} else if (slot == EQ::textures::TextureSlot::weaponSecondary) {
|
|
d_melee_texture2 = texture;
|
|
} else {
|
|
// Reserved for texturing individual armor slots
|
|
}
|
|
}
|
|
|
|
uint32 NPC::GetSwarmOwner()
|
|
{
|
|
if(GetSwarmInfo() != nullptr)
|
|
{
|
|
return GetSwarmInfo()->owner_id;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint32 NPC::GetSwarmTarget()
|
|
{
|
|
if(GetSwarmInfo() != nullptr)
|
|
{
|
|
return GetSwarmInfo()->target;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void NPC::SetSwarmTarget(int target_id)
|
|
{
|
|
if(GetSwarmInfo() != nullptr)
|
|
{
|
|
GetSwarmInfo()->target = target_id;
|
|
}
|
|
}
|
|
|
|
int64 NPC::CalcMaxMana()
|
|
{
|
|
if (npc_mana == 0) {
|
|
if (IsIntelligenceCasterClass()) {
|
|
max_mana = (((GetINT() / 2) + 1) * GetLevel()) + spellbonuses.Mana + itembonuses.Mana;
|
|
} else if (IsWisdomCasterClass()) {
|
|
max_mana = (((GetWIS() / 2) + 1) * GetLevel()) + spellbonuses.Mana + itembonuses.Mana;
|
|
} else {
|
|
max_mana = 0;
|
|
}
|
|
|
|
if (max_mana < 0) {
|
|
max_mana = 0;
|
|
}
|
|
|
|
return max_mana;
|
|
} else {
|
|
if (IsIntelligenceCasterClass()) {
|
|
max_mana = npc_mana + spellbonuses.Mana + itembonuses.Mana;
|
|
} else if (IsWisdomCasterClass()) {
|
|
max_mana = npc_mana + spellbonuses.Mana + itembonuses.Mana;
|
|
} else {
|
|
max_mana = 0;
|
|
}
|
|
|
|
if (max_mana < 0) {
|
|
max_mana = 0;
|
|
}
|
|
|
|
return max_mana;
|
|
}
|
|
}
|
|
|
|
void NPC::SignalNPC(int _signal_id)
|
|
{
|
|
signal_q.push_back(_signal_id);
|
|
}
|
|
|
|
void NPC::SendPayload(int payload_id, std::string payload_value)
|
|
{
|
|
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_PAYLOAD)) {
|
|
const auto& export_string = fmt::format("{} {}", payload_id, payload_value);
|
|
|
|
parse->EventNPC(EVENT_PAYLOAD, this, nullptr, export_string, 0);
|
|
}
|
|
}
|
|
|
|
NPC_Emote_Struct* NPC::GetNPCEmote(uint32 emote_id, uint8 event_) {
|
|
std::vector<NPC_Emote_Struct*> emotes;
|
|
|
|
for (auto& e : zone->npc_emote_list) {
|
|
if (e->emoteid == emote_id && e->event_ == event_) {
|
|
emotes.emplace_back(e);
|
|
}
|
|
}
|
|
|
|
if (emotes.empty()) {
|
|
return nullptr;
|
|
} else if (emotes.size() == 1) {
|
|
return emotes.front();
|
|
}
|
|
|
|
const int index = zone->random.Roll0(emotes.size());
|
|
|
|
return emotes[index];
|
|
}
|
|
|
|
void NPC::DoNPCEmote(uint8 event_, uint32 emote_id, Mob* t)
|
|
{
|
|
if (!emote_id) {
|
|
return;
|
|
}
|
|
|
|
const auto& e = GetNPCEmote(emote_id, event_);
|
|
if (!e) {
|
|
return;
|
|
}
|
|
|
|
std::string processed = e->text;
|
|
|
|
// Mob Variables
|
|
Strings::FindReplace(processed, "$mname", GetCleanName());
|
|
Strings::FindReplace(processed, "$mracep", GetRacePlural());
|
|
Strings::FindReplace(processed, "$mrace", GetPlayerRaceName(GetRace()));
|
|
Strings::FindReplace(processed, "$mclass", GetClassIDName(GetClass()));
|
|
Strings::FindReplace(processed, "$mclassp", GetClassPlural());
|
|
|
|
// Target Variables
|
|
Strings::FindReplace(processed, "$name", t ? t->GetCleanName() : "foe");
|
|
Strings::FindReplace(processed, "$class", t ? GetClassIDName(t->GetClass()) : "class");
|
|
Strings::FindReplace(processed, "$classp", t ? t->GetClassPlural() : "classes");
|
|
Strings::FindReplace(processed, "$race", t ? GetPlayerRaceName(t->GetRace()) : "race");
|
|
Strings::FindReplace(processed, "$racep", t ? t->GetRacePlural() : "races");
|
|
|
|
if (emoteid == e->emoteid) {
|
|
if (event_ == EQ::constants::EmoteEventTypes::Hailed && t) {
|
|
DoQuestPause(t);
|
|
}
|
|
|
|
if (e->type == EQ::constants::EmoteTypes::Say) {
|
|
Say(processed.c_str());
|
|
} else if (e->type == EQ::constants::EmoteTypes::Emote) {
|
|
Emote(processed.c_str());
|
|
} else if (e->type == EQ::constants::EmoteTypes::Shout) {
|
|
Shout(processed.c_str());
|
|
} else if (e->type == EQ::constants::EmoteTypes::Proximity) {
|
|
entity_list.MessageCloseString(
|
|
this,
|
|
true,
|
|
200,
|
|
Chat::NPCQuestSay,
|
|
GENERIC_STRING,
|
|
processed.c_str()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NPC::CanTalk()
|
|
{
|
|
switch (GetRace()) {
|
|
case Race::Human:
|
|
case Race::Barbarian:
|
|
case Race::Erudite:
|
|
case Race::WoodElf:
|
|
case Race::HighElf:
|
|
case Race::DarkElf:
|
|
case Race::HalfElf:
|
|
case Race::Dwarf:
|
|
case Race::Troll:
|
|
case Race::Ogre:
|
|
case Race::Halfling:
|
|
case Race::Gnome:
|
|
case Race::Werewolf:
|
|
case Race::Brownie:
|
|
case Race::Centaur:
|
|
case Race::Giant:
|
|
case Race::Trakanon:
|
|
case Race::VenrilSathir:
|
|
case Race::Kerran:
|
|
case Race::Fairy:
|
|
case Race::Ghost:
|
|
case Race::Gnoll:
|
|
case Race::Goblin:
|
|
case Race::FreeportGuard:
|
|
case Race::LavaDragon:
|
|
case Race::LizardMan:
|
|
case Race::Minotaur:
|
|
case Race::Orc:
|
|
case Race::HumanBeggar:
|
|
case Race::Pixie:
|
|
case Race::Drachnid:
|
|
case Race::SolusekRo:
|
|
case Race::Tunare:
|
|
case Race::Treant:
|
|
case Race::Vampire:
|
|
case Race::StatueOfRallosZek:
|
|
case Race::HighpassCitizen:
|
|
case Race::Zombie:
|
|
case Race::QeynosCitizen:
|
|
case Race::NeriakCitizen:
|
|
case Race::EruditeCitizen:
|
|
case Race::Bixie:
|
|
case Race::RivervaleCitizen:
|
|
case Race::Scarecrow:
|
|
case Race::Sphinx:
|
|
case Race::HalasCitizen:
|
|
case Race::GrobbCitizen:
|
|
case Race::OggokCitizen:
|
|
case Race::KaladimCitizen:
|
|
case Race::CazicThule:
|
|
case Race::ElfVampire:
|
|
case Race::Denizen:
|
|
case Race::Efreeti:
|
|
case Race::PhinigelAutropos:
|
|
case Race::Mermaid:
|
|
case Race::Harpy:
|
|
case Race::Fayguard:
|
|
case Race::Innoruuk:
|
|
case Race::Djinn:
|
|
case Race::InvisibleMan:
|
|
case Race::Iksar:
|
|
case Race::VahShir:
|
|
case Race::Sarnak:
|
|
case Race::Xalgoz:
|
|
case Race::Yeti:
|
|
case Race::IksarCitizen:
|
|
case Race::ForestGiant:
|
|
case Race::Burynai:
|
|
case Race::Erollisi:
|
|
case Race::Tribunal:
|
|
case Race::Bertoxxulous:
|
|
case Race::Bristlebane:
|
|
case Race::Ratman:
|
|
case Race::Coldain:
|
|
case Race::VeliousDragon:
|
|
case Race::Siren:
|
|
case Race::FrostGiant:
|
|
case Race::StormGiant:
|
|
case Race::BlackAndWhiteDragon:
|
|
case Race::GhostDragon:
|
|
case Race::PrismaticDragon:
|
|
case Race::Grimling:
|
|
case Race::KhatiSha:
|
|
case Race::Vampire2:
|
|
case Race::Shissar:
|
|
case Race::VampireVolatalis:
|
|
case Race::Shadel:
|
|
case Race::Netherbian:
|
|
case Race::Akhevan:
|
|
case Race::Wretch:
|
|
case Race::LordInquisitorSeru:
|
|
case Race::VahShirKing:
|
|
case Race::VahShirGuard:
|
|
case Race::TeleportMan:
|
|
case Race::Werewolf2:
|
|
case Race::Nymph:
|
|
case Race::Dryad:
|
|
case Race::Treant2:
|
|
case Race::TarewMarr:
|
|
case Race::SolusekRo2:
|
|
case Race::GuardOfJustice:
|
|
case Race::SolusekRoGuard:
|
|
case Race::BertoxxulousNew:
|
|
case Race::TribunalNew:
|
|
case Race::TerrisThule:
|
|
case Race::KnightOfPestilence:
|
|
case Race::Lepertoloth:
|
|
case Race::Pusling:
|
|
case Race::WaterMephit:
|
|
case Race::NightmareGoblin:
|
|
case Race::Karana:
|
|
case Race::Saryrn:
|
|
case Race::FenninRo:
|
|
case Race::SoulDevourer:
|
|
case Race::NewRallosZek:
|
|
case Race::VallonZek:
|
|
case Race::TallonZek:
|
|
case Race::AirMephit:
|
|
case Race::EarthMephit:
|
|
case Race::FireMephit:
|
|
case Race::NightmareMephit:
|
|
case Race::Zebuxoruk:
|
|
case Race::MithanielMarr:
|
|
case Race::UndeadKnight:
|
|
case Race::Rathe:
|
|
case Race::Xegony:
|
|
case Race::Fiend:
|
|
case Race::Quarm:
|
|
case Race::Efreeti2:
|
|
case Race::Valorian2:
|
|
case Race::AnimatedArmor:
|
|
case Race::UndeadFootman:
|
|
case Race::RallosOgre:
|
|
case Race::Froglok2:
|
|
case Race::TrollCrewMember:
|
|
case Race::PirateDeckhand:
|
|
case Race::BrokenSkullPirate:
|
|
case Race::PirateGhost:
|
|
case Race::OneArmedPirate:
|
|
case Race::SpiritmasterNadox:
|
|
case Race::BrokenSkullTaskmaster:
|
|
case Race::GnomePirate:
|
|
case Race::DarkElfPirate:
|
|
case Race::OgrePirate:
|
|
case Race::HumanPirate:
|
|
case Race::EruditePirate:
|
|
case Race::UndeadVampire:
|
|
case Race::Vampire3:
|
|
case Race::RujarkianOrc:
|
|
case Race::BoneGolem:
|
|
case Race::SandElf:
|
|
case Race::MasterVampire:
|
|
case Race::MasterOrc:
|
|
case Race::Mummy:
|
|
case Race::NewGoblin:
|
|
case Race::Nihil:
|
|
case Race::Trusik:
|
|
case Race::Ukun:
|
|
case Race::Ixt:
|
|
case Race::Ikaav:
|
|
case Race::Aneuk:
|
|
case Race::Kyv:
|
|
case Race::Noc:
|
|
case Race::Ratuk:
|
|
case Race::Huvul:
|
|
case Race::Mastruq:
|
|
case Race::MataMuram:
|
|
case Race::Succubus:
|
|
case Race::Pyrilen:
|
|
case Race::Dragorn:
|
|
case Race::Gelidran:
|
|
case Race::Minotaur2:
|
|
case Race::CrystalShard:
|
|
case Race::Goblin2:
|
|
case Race::Giant2:
|
|
case Race::Orc2:
|
|
case Race::Werewolf3:
|
|
case Race::Shiliskin:
|
|
case Race::Minotaur3:
|
|
case Race::Fairy2:
|
|
case Race::Bolvirk:
|
|
case Race::Elddar:
|
|
case Race::ForestGiant2:
|
|
case Race::BoneGolem2:
|
|
case Race::Scrykin:
|
|
case Race::Treant3:
|
|
case Race::Vampire4:
|
|
case Race::AyonaeRo:
|
|
case Race::SullonZek:
|
|
case Race::Bixie2:
|
|
case Race::Centaur2:
|
|
case Race::Drakkin:
|
|
case Race::Giant3:
|
|
case Race::Gnoll2:
|
|
case Race::GiantShade:
|
|
case Race::Harpy2:
|
|
case Race::Satyr:
|
|
case Race::Dynleth:
|
|
case Race::Kedge:
|
|
case Race::Kerran2:
|
|
case Race::Shissar2:
|
|
case Race::Siren2:
|
|
case Race::Sphinx2:
|
|
case Race::Human2:
|
|
case Race::Brownie2:
|
|
case Race::Exoskeleton:
|
|
case Race::Minotaur4:
|
|
case Race::Scarecrow2:
|
|
case Race::Wereorc:
|
|
case Race::ElvenGhost:
|
|
case Race::HumanGhost:
|
|
case Race::Burynai2:
|
|
case Race::Dracolich:
|
|
case Race::IksarGhost:
|
|
case Race::Mephit:
|
|
case Race::Sarnak2:
|
|
case Race::Gnoll3:
|
|
case Race::GodOfDiscord:
|
|
case Race::Ogre2:
|
|
case Race::Giant4:
|
|
case Race::Apexus:
|
|
case Race::Bellikos:
|
|
case Race::BrellsFirstCreation:
|
|
case Race::Brell:
|
|
case Race::Coldain2:
|
|
case Race::Coldain3:
|
|
case Race::Telmira:
|
|
case Race::MorellThule:
|
|
case Race::Amygdalan:
|
|
case Race::Sandman:
|
|
case Race::RoyalGuard:
|
|
case Race::CazicThule2:
|
|
case Race::Erudite2:
|
|
case Race::Alaran:
|
|
case Race::AlaranGhost:
|
|
case Race::Ratman2:
|
|
case Race::Akheva:
|
|
case Race::Luclin:
|
|
case Race::Luclin2:
|
|
case Race::Luclin3:
|
|
case Race::Luclin4:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//this is called with 'this' as the mob being looked at, and
|
|
//iOther the mob who is doing the looking. It should figure out
|
|
//what iOther thinks about 'this'
|
|
FACTION_VALUE NPC::GetReverseFactionCon(Mob* iOther) {
|
|
iOther = iOther->GetOwnerOrSelf();
|
|
int primaryFaction= iOther->GetPrimaryFaction();
|
|
|
|
//I am pretty sure that this special faction call is backwards
|
|
//and should be iOther->GetSpecialFactionCon(this)
|
|
if (primaryFaction < 0)
|
|
return GetSpecialFactionCon(iOther);
|
|
|
|
if (primaryFaction == 0)
|
|
return FACTION_INDIFFERENTLY;
|
|
|
|
//if we are a pet, use our owner's faction stuff
|
|
Mob *own = GetOwner();
|
|
if (own != nullptr)
|
|
return own->GetReverseFactionCon(iOther);
|
|
|
|
//make sure iOther is an npc
|
|
//also, if we dont have a faction, then they arnt gunna think anything of us either
|
|
if(!iOther->IsNPC() || GetPrimaryFaction() == 0)
|
|
return(FACTION_INDIFFERENTLY);
|
|
|
|
//if we get here, iOther is an NPC too
|
|
|
|
//otherwise, employ the npc faction stuff
|
|
//so we need to look at iOther's faction table to see
|
|
//what iOther thinks about our primary faction
|
|
return(iOther->CastToNPC()->CheckNPCFactionAlly(GetPrimaryFaction()));
|
|
}
|
|
|
|
//Look through our faction list and return a faction con based
|
|
//on the npc_value for the other person's primary faction in our list.
|
|
FACTION_VALUE NPC::CheckNPCFactionAlly(int32 other_faction)
|
|
{
|
|
for (const auto& e : faction_list) {
|
|
if (e.faction_id == other_faction) {
|
|
if (e.npc_value > 0) {
|
|
return FACTION_ALLY;
|
|
} else if (e.npc_value < 0) {
|
|
return FACTION_SCOWLS;
|
|
} else {
|
|
return FACTION_INDIFFERENTLY;
|
|
}
|
|
}
|
|
}
|
|
|
|
// I believe that the assumption is, barring no entry in npc_faction_entries
|
|
// that two npcs on like faction con ally to each other. This catches cases
|
|
// where an npc is on a faction but has no hits (hence no entry in
|
|
// npc_faction_entries).
|
|
|
|
if (GetPrimaryFaction() == other_faction) {
|
|
return FACTION_ALLY;
|
|
} else {
|
|
return FACTION_INDIFFERENTLY;
|
|
}
|
|
}
|
|
|
|
bool NPC::IsFactionListAlly(uint32 other_faction)
|
|
{
|
|
return CheckNPCFactionAlly(other_faction) == FACTION_ALLY;
|
|
}
|
|
|
|
int NPC::GetScore()
|
|
{
|
|
int lv = std::min(70, (int)GetLevel());
|
|
int basedmg = (lv*2)*(1+(lv / 100)) - (lv / 2);
|
|
int minx = 0;
|
|
int64 basehp = 0;
|
|
int hpcontrib = 0;
|
|
int dmgcontrib = 0;
|
|
int spccontrib = 0;
|
|
int64 hp = GetMaxHP();
|
|
int mindmg = min_dmg;
|
|
int maxdmg = max_dmg;
|
|
int final;
|
|
|
|
if(lv < 46)
|
|
{
|
|
minx = static_cast<int> (ceil( ((lv - (lv / 10.0)) - 1.0) ));
|
|
basehp = (lv * 10) + (lv * lv);
|
|
}
|
|
else
|
|
{
|
|
minx = static_cast<int> (ceil( ((lv - (lv / 10.0)) - 1.0) - (( lv - 45.0 ) / 2.0) ));
|
|
basehp = (lv * 10) + ((lv * lv) * 4);
|
|
}
|
|
|
|
if(hp > basehp)
|
|
{
|
|
hpcontrib = static_cast<int> (((hp / static_cast<float> (basehp)) * 1.5));
|
|
if(hpcontrib > 5) { hpcontrib = 5; }
|
|
|
|
if(maxdmg > basedmg)
|
|
{
|
|
dmgcontrib = static_cast<int> (ceil( ((maxdmg / basedmg) * 1.5) ));
|
|
}
|
|
|
|
if(HasNPCSpecialAtk("E")) { spccontrib++; } //Enrage
|
|
if(HasNPCSpecialAtk("F")) { spccontrib++; } //Flurry
|
|
if(HasNPCSpecialAtk("R")) { spccontrib++; } //Rampage
|
|
if(HasNPCSpecialAtk("r")) { spccontrib++; } //Area Rampage
|
|
if(HasNPCSpecialAtk("S")) { spccontrib++; } //Summon
|
|
if(HasNPCSpecialAtk("T")) { spccontrib += 2; } //Triple
|
|
if(HasNPCSpecialAtk("Q")) { spccontrib += 3; } //Quad
|
|
if(HasNPCSpecialAtk("U")) { spccontrib += 5; } //SpecialAbility::SlowImmunity
|
|
if(HasNPCSpecialAtk("L")) { spccontrib++; } //Innate Dual Wield
|
|
}
|
|
|
|
if(npc_spells_id > 12)
|
|
{
|
|
if(lv < 16)
|
|
spccontrib++;
|
|
else
|
|
spccontrib += static_cast<int> (floor(lv/15.0));
|
|
}
|
|
|
|
final = minx + hpcontrib + dmgcontrib + spccontrib;
|
|
final = std::max(1, final);
|
|
final = std::min(100, final);
|
|
return(final);
|
|
}
|
|
|
|
uint32 NPC::GetSpawnKillCount()
|
|
{
|
|
uint32 sid = GetSpawnPointID();
|
|
|
|
if(sid > 0)
|
|
{
|
|
return(zone->GetSpawnKillCount(sid));
|
|
}
|
|
|
|
return(0);
|
|
}
|
|
|
|
void NPC::DoQuestPause(Mob* m)
|
|
{
|
|
if (!m) {
|
|
return;
|
|
}
|
|
|
|
if (IsMoving() && !IsOnHatelist(m)) {
|
|
PauseWandering(RuleI(NPC, SayPauseTimeInSec));
|
|
|
|
if (FacesTarget() && !m->sneaking) {
|
|
FaceTarget(m);
|
|
}
|
|
} else if (!IsMoving()) {
|
|
if (
|
|
FacesTarget() &&
|
|
!m->sneaking &&
|
|
GetAppearance() != eaSitting &&
|
|
GetAppearance() != eaDead
|
|
) {
|
|
FaceTarget(m);
|
|
}
|
|
}
|
|
}
|
|
|
|
void NPC::ChangeLastName(std::string last_name)
|
|
{
|
|
auto outapp = new EQApplicationPacket(OP_GMLastName, sizeof(GMLastName_Struct));
|
|
auto gmn = (GMLastName_Struct*) outapp->pBuffer;
|
|
|
|
strn0cpy(gmn->name, GetName(), sizeof(gmn->name));
|
|
strn0cpy(gmn->gmname, GetName(), sizeof(gmn->gmname));
|
|
strn0cpy(gmn->lastname, last_name.c_str(), sizeof(gmn->lastname));
|
|
|
|
gmn->unknown[0] = 1;
|
|
gmn->unknown[1] = 1;
|
|
gmn->unknown[2] = 1;
|
|
gmn->unknown[3] = 1;
|
|
|
|
entity_list.QueueClients(this, outapp, false);
|
|
|
|
safe_delete(outapp);
|
|
}
|
|
|
|
void NPC::ClearLastName()
|
|
{
|
|
std::string empty;
|
|
ChangeLastName(empty);
|
|
}
|
|
|
|
void NPC::DepopSwarmPets()
|
|
{
|
|
if (GetSwarmInfo()) {
|
|
if (GetSwarmInfo()->duration->Check(false)){
|
|
Mob* owner = entity_list.GetMobID(GetSwarmInfo()->owner_id);
|
|
if (owner) {
|
|
owner->SetTempPetCount(owner->GetTempPetCount() - 1);
|
|
}
|
|
Depop();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NPC::ModifyStatsOnCharm(bool is_charm_removed)
|
|
{
|
|
if (is_charm_removed) {
|
|
if (charm_ac) {
|
|
AC = default_ac;
|
|
}
|
|
if (charm_attack_delay) {
|
|
attack_delay = default_attack_delay;
|
|
}
|
|
if (charm_accuracy_rating) {
|
|
accuracy_rating = default_accuracy_rating;
|
|
}
|
|
if (charm_avoidance_rating) {
|
|
avoidance_rating = default_avoidance_rating;
|
|
}
|
|
if (charm_atk) {
|
|
ATK = default_atk;
|
|
}
|
|
if (charm_min_dmg || charm_max_dmg) {
|
|
base_damage = round((default_max_dmg - default_min_dmg) / 1.9);
|
|
min_damage = default_min_dmg - round(base_damage / 10.0);
|
|
}
|
|
if (RuleB(Spells, CharmDisablesSpecialAbilities)) {
|
|
ProcessSpecialAbilities(default_special_abilities);
|
|
}
|
|
|
|
SetAttackTimer();
|
|
CalcAC();
|
|
|
|
return;
|
|
}
|
|
|
|
if (charm_ac) {
|
|
AC = charm_ac;
|
|
}
|
|
if (charm_attack_delay) {
|
|
attack_delay = charm_attack_delay;
|
|
}
|
|
if (charm_accuracy_rating) {
|
|
accuracy_rating = charm_accuracy_rating;
|
|
}
|
|
if (charm_avoidance_rating) {
|
|
avoidance_rating = charm_avoidance_rating;
|
|
}
|
|
if (charm_atk) {
|
|
ATK = charm_atk;
|
|
}
|
|
if (charm_min_dmg || charm_max_dmg) {
|
|
base_damage = round((charm_max_dmg - charm_min_dmg) / 1.9);
|
|
min_damage = charm_min_dmg - round(base_damage / 10.0);
|
|
}
|
|
if (RuleB(Spells, CharmDisablesSpecialAbilities)) {
|
|
ClearSpecialAbilities();
|
|
}
|
|
|
|
// the rest of the stats aren't cached, so lets just do these two instead of full CalcBonuses()
|
|
SetAttackTimer();
|
|
CalcAC();
|
|
}
|
|
|
|
uint16 NPC::GetMeleeTexture1() const
|
|
{
|
|
return d_melee_texture1;
|
|
}
|
|
|
|
uint16 NPC::GetMeleeTexture2() const
|
|
{
|
|
return d_melee_texture2;
|
|
}
|
|
|
|
float NPC::GetProximityMinX()
|
|
{
|
|
return proximity->min_x;
|
|
}
|
|
|
|
float NPC::GetProximityMaxX()
|
|
{
|
|
return proximity->max_x;
|
|
}
|
|
|
|
float NPC::GetProximityMinY()
|
|
{
|
|
return proximity->min_y;
|
|
}
|
|
|
|
float NPC::GetProximityMaxY()
|
|
{
|
|
return proximity->max_y;
|
|
}
|
|
|
|
float NPC::GetProximityMinZ()
|
|
{
|
|
return proximity->min_z;
|
|
}
|
|
|
|
float NPC::GetProximityMaxZ()
|
|
{
|
|
return proximity->max_z;
|
|
}
|
|
|
|
bool NPC::IsProximitySet()
|
|
{
|
|
if (proximity && proximity->proximity_set) {
|
|
return proximity->proximity_set;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param box_size
|
|
* @param move_distance
|
|
* @param move_delay
|
|
*/
|
|
void NPC::SetSimpleRoamBox(float box_size, float move_distance, int move_delay)
|
|
{
|
|
AI_SetRoambox(
|
|
(move_distance != 0 ? move_distance : box_size / 2),
|
|
GetX() + box_size,
|
|
GetX() - box_size,
|
|
GetY() + box_size,
|
|
GetY() - box_size,
|
|
move_delay
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param caster
|
|
* @param chance
|
|
* @param cast_range
|
|
* @param spell_types
|
|
* @return
|
|
*/
|
|
bool NPC::AICheckCloseBeneficialSpells(
|
|
NPC *caster,
|
|
uint8 chance,
|
|
float cast_range,
|
|
uint32 spell_types
|
|
)
|
|
{
|
|
if((spell_types & SPELL_TYPES_DETRIMENTAL) != 0) {
|
|
LogError("Detrimental spells requested from AICheckCloseBeneficialSpells!");
|
|
return false;
|
|
}
|
|
|
|
if (!caster) {
|
|
return false;
|
|
}
|
|
|
|
if (!caster->AI_HasSpells()) {
|
|
return false;
|
|
}
|
|
|
|
if (caster->GetSpecialAbility(SpecialAbility::NoBuffHealFriends)) {
|
|
return false;
|
|
}
|
|
|
|
if (chance < 100) {
|
|
uint8 tmp = zone->random.Int(0, 99);
|
|
if (tmp >= chance) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indifferent
|
|
*/
|
|
if (caster->GetPrimaryFaction() == 0) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check through close range mobs
|
|
*/
|
|
for (auto & close_mob : caster->GetCloseMobList(cast_range)) {
|
|
Mob *mob = close_mob.second;
|
|
if (!mob) {
|
|
continue;
|
|
}
|
|
|
|
if (mob->IsClient()) {
|
|
continue;
|
|
}
|
|
|
|
float distance = Distance(mob->GetPosition(), caster->GetPosition());
|
|
if (distance > cast_range) {
|
|
continue;
|
|
}
|
|
|
|
if (!mob->CheckLosFN(caster)) {
|
|
continue;
|
|
}
|
|
|
|
if (mob->GetReverseFactionCon(caster) >= FACTION_KINDLY) {
|
|
continue;
|
|
}
|
|
|
|
LogAICastBeneficialClose(
|
|
"NPC [{}] Distance [{}] Cast Range [{}] Caster [{}]",
|
|
mob->GetCleanName(),
|
|
distance,
|
|
cast_range,
|
|
caster->GetCleanName()
|
|
);
|
|
|
|
if ((spell_types & SpellType_Buff) && !RuleB(NPC, BuffFriends)) {
|
|
if (mob != caster) {
|
|
spell_types = SpellType_Heal;
|
|
}
|
|
}
|
|
|
|
if (caster->AICastSpell(mob, 100, spell_types)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param sender
|
|
* @param attacker
|
|
*/
|
|
void NPC::AIYellForHelp(Mob *sender, Mob *attacker)
|
|
{
|
|
LogAIYellForHelp("Mob[{}] Target[{}]",
|
|
(sender == nullptr ? "NULL MOB" : GetCleanName()),
|
|
(attacker == nullptr ? "NULL TARGET" : attacker->GetCleanName())
|
|
);
|
|
|
|
if (!sender || !attacker) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* If we dont have a faction set, we're gonna be indiff to everybody
|
|
*/
|
|
if (sender->GetPrimaryFaction() == 0) {
|
|
LogAIYellForHelp("No Primary Faction");
|
|
return;
|
|
}
|
|
|
|
if (sender->HasAssistAggro()) {
|
|
LogAIYellForHelp("I have assist aggro");
|
|
return;
|
|
}
|
|
|
|
LogAIYellForHelp(
|
|
"NPC [{}] ID [{}] is starting to scan",
|
|
GetCleanName(),
|
|
GetID()
|
|
);
|
|
|
|
for (auto &close_mob: sender->GetCloseMobList()) {
|
|
Mob *mob = close_mob.second;
|
|
if (!mob) {
|
|
continue;
|
|
}
|
|
|
|
float distance = DistanceSquared(m_Position, mob->GetPosition());
|
|
|
|
if (mob->IsClient()) {
|
|
continue;
|
|
}
|
|
|
|
float assist_range = (mob->GetAssistRange() * mob->GetAssistRange());
|
|
|
|
// Implement optional sneak-pull
|
|
if (RuleB(Combat, EnableSneakPull) && attacker->sneaking) {
|
|
assist_range = RuleI(Combat, SneakPullAssistRange);
|
|
if (attacker->IsClient()) {
|
|
float clientx = attacker->GetX();
|
|
float clienty = attacker->GetY();
|
|
if (attacker->CastToClient()->BehindMob(mob, clientx, clienty)) {
|
|
assist_range = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (distance > assist_range) {
|
|
continue;
|
|
}
|
|
|
|
LogAIYellForHelpDetail(
|
|
"NPC [{}] ID [{}] is scanning - checking against NPC [{}] range [{}] dist [{}] in_range [{}]",
|
|
GetCleanName(),
|
|
GetID(),
|
|
mob->GetCleanName(),
|
|
assist_range,
|
|
distance,
|
|
(distance < assist_range)
|
|
);
|
|
|
|
if (mob->CheckAggro(attacker)) {
|
|
continue;
|
|
}
|
|
|
|
if (sender->NPCAssistCap() >= RuleI(Combat, NPCAssistCap)) {
|
|
break;
|
|
}
|
|
|
|
if (
|
|
mob != sender
|
|
&& mob != attacker
|
|
&& mob->GetPrimaryFaction() != 0
|
|
&& !mob->IsEngaged()
|
|
&& ((!mob->IsPet()) || (mob->IsPet() && mob->GetOwner() && !mob->GetOwner()->IsOfClientBot()))
|
|
) {
|
|
|
|
/**
|
|
* if they are in range, make sure we are not green...
|
|
* then jump in if they are our friend
|
|
*/
|
|
if (mob->GetLevel() >= 50 || mob->AlwaysAggro() || attacker->GetLevelCon(mob->GetLevel()) != ConsiderColor::Gray) {
|
|
if (mob->GetPrimaryFaction() == sender->CastToNPC()->GetPrimaryFaction()) {
|
|
const auto f = zone->GetNPCFaction(mob->CastToNPC()->GetNPCFactionID());
|
|
if (f) {
|
|
if (f->ignore_primary_assist) {
|
|
continue; //Same faction and ignore primary assist
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sender->GetReverseFactionCon(mob) <= FACTION_AMIABLY) {
|
|
//attacking someone on same faction, or a friend
|
|
//Father Nitwit: make sure we can see them.
|
|
if (mob->CheckLosFN(sender)) {
|
|
mob->AddToHateList(attacker, 25, 0, false);
|
|
sender->AddAssistCap();
|
|
|
|
LogAIYellForHelpDetail(
|
|
"NPC [{}] is assisting [{}] against target [{}]",
|
|
mob->GetCleanName(),
|
|
GetCleanName(),
|
|
attacker->GetCleanName()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void NPC::RecalculateSkills()
|
|
{
|
|
int r;
|
|
for (r = 0; r <= EQ::skills::HIGHEST_SKILL; r++) {
|
|
skills[r] = skill_caps.GetSkillCap(GetClass(), (EQ::skills::SkillType)r, level).cap;
|
|
}
|
|
|
|
// some overrides -- really we need to be able to set skills for mobs in the DB
|
|
// There are some known low level SHM/BST pets that do not follow this, which supports
|
|
// the theory of needing to be able to set skills for each mob separately
|
|
if (!IsBot()) {
|
|
if (level > 50) {
|
|
skills[EQ::skills::SkillDoubleAttack] = 250;
|
|
skills[EQ::skills::SkillDualWield] = 250;
|
|
}
|
|
else if (level > 3) {
|
|
skills[EQ::skills::SkillDoubleAttack] = level * 5;
|
|
skills[EQ::skills::SkillDualWield] = skills[EQ::skills::SkillDoubleAttack];
|
|
}
|
|
else {
|
|
skills[EQ::skills::SkillDoubleAttack] = level * 5;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NPC::ReloadSpells() {
|
|
AI_AddNPCSpells(GetNPCSpellsID());
|
|
AI_AddNPCSpellsEffects(GetNPCSpellsEffectsID());
|
|
}
|
|
|
|
void NPC::ScaleNPC(uint8 npc_level, bool always_scale, bool override_special_abilities) {
|
|
if (GetLevel() != npc_level) {
|
|
SetLevel(npc_level);
|
|
RecalculateSkills();
|
|
ReloadSpells();
|
|
}
|
|
|
|
npc_scale_manager->ResetNPCScaling(this);
|
|
npc_scale_manager->ScaleNPC(this, always_scale, override_special_abilities);
|
|
}
|
|
|
|
bool NPC::IsGuard()
|
|
{
|
|
switch (GetRace()) {
|
|
case Race::FreeportGuard:
|
|
if (GetTexture() == 1 || GetTexture() == 2)
|
|
return true;
|
|
break;
|
|
case Race::IksarCitizen:
|
|
if (GetTexture() == 1)
|
|
return true;
|
|
break;
|
|
case Race::Felguard:
|
|
case Race::Fayguard:
|
|
case Race::VahShirGuard:
|
|
case Race::QeynosCitizen:
|
|
case Race::RivervaleCitizen:
|
|
case Race::EruditeCitizen:
|
|
case Race::HalasCitizen:
|
|
case Race::NeriakCitizen:
|
|
case Race::GrobbCitizen:
|
|
case OGGOK_CITIZEN:
|
|
case Race::KaladimCitizen:
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
if (GetPrimaryFaction() == DB_FACTION_GEM_CHOPPERS || GetPrimaryFaction() == DB_FACTION_HERETICS || GetPrimaryFaction() == DB_FACTION_KING_AKANON) { //these 3 factions of guards use player races instead of their own races so we must define them by faction.
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::vector<int> NPC::GetLootList() {
|
|
std::vector<int> npc_items;
|
|
for (auto current_item = m_loot_items.begin(); current_item != m_loot_items.end(); ++current_item) {
|
|
LootItem * loot_item = *current_item;
|
|
if (!loot_item) {
|
|
LogError("NPC::GetLootList() - ItemList error, null item");
|
|
continue;
|
|
}
|
|
|
|
if (std::find(npc_items.begin(), npc_items.end(), loot_item->item_id) != npc_items.end()) {
|
|
continue;
|
|
}
|
|
|
|
npc_items.push_back(loot_item->item_id);
|
|
}
|
|
return npc_items;
|
|
}
|
|
|
|
bool NPC::IsRecordLootStats() const
|
|
{
|
|
return m_record_loot_stats;
|
|
}
|
|
|
|
void NPC::SetRecordLootStats(bool record_loot_stats)
|
|
{
|
|
NPC::m_record_loot_stats = record_loot_stats;
|
|
}
|
|
|
|
const std::vector<uint32> &NPC::GetRolledItems() const
|
|
{
|
|
return m_rolled_items;
|
|
}
|
|
|
|
int NPC::GetRolledItemCount(uint32 item_id)
|
|
{
|
|
int rolled_count = 0;
|
|
for (auto &e: m_rolled_items) {
|
|
if (item_id == e) {
|
|
rolled_count++;
|
|
}
|
|
}
|
|
|
|
return rolled_count;
|
|
}
|
|
|
|
void NPC::SendPositionToClients()
|
|
{
|
|
static EQApplicationPacket p(OP_ClientUpdate, sizeof(PlayerPositionUpdateServer_Struct));
|
|
auto *s = (PlayerPositionUpdateServer_Struct *) p.pBuffer;
|
|
for (auto &c: entity_list.GetClientList()) {
|
|
MakeSpawnUpdate(s);
|
|
c.second->QueuePacket(&p, false);
|
|
}
|
|
}
|
|
|
|
void NPC::HandleRoambox()
|
|
{
|
|
bool has_arrived = GetCWP() == EQ::WaypointStatus::RoamBoxPauseInProgress && !IsMoving();
|
|
if (has_arrived) {
|
|
int roambox_move_delay = EQ::ClampLower(GetRoamboxDelay(), GetRoamboxMinDelay());
|
|
int move_delay_max = (roambox_move_delay > 0 ? roambox_move_delay : (int) GetRoamboxMinDelay() * 4);
|
|
int random_timer = RandomTimer(
|
|
GetRoamboxMinDelay(),
|
|
move_delay_max
|
|
);
|
|
|
|
LogNPCRoamBoxDetail(
|
|
"({}) random_timer [{}] roambox_move_delay [{}] move_min [{}] move_max [{}]",
|
|
GetCleanName(),
|
|
random_timer,
|
|
roambox_move_delay,
|
|
(int) GetRoamboxMinDelay(),
|
|
move_delay_max
|
|
);
|
|
|
|
time_until_can_move = Timer::GetCurrentTime() + random_timer;
|
|
SetCurrentWP(0);
|
|
return;
|
|
}
|
|
|
|
bool ready_to_set_new_destination = !IsMoving() && time_until_can_move < Timer::GetCurrentTime();
|
|
if (ready_to_set_new_destination) {
|
|
// make several attempts to find a valid next move in the box
|
|
bool can_path = false;
|
|
for (int i = 0; i < 10; i++) {
|
|
auto move_x = static_cast<float>(zone->random.Real(-m_roambox.distance, m_roambox.distance));
|
|
auto move_y = static_cast<float>(zone->random.Real(-m_roambox.distance, m_roambox.distance));
|
|
auto requested_x = EQ::Clamp((GetX() + move_x), m_roambox.min_x, m_roambox.max_x);
|
|
auto requested_y = EQ::Clamp((GetY() + move_y), m_roambox.min_y, m_roambox.max_y);
|
|
auto requested_z = GetGroundZ(requested_x, requested_y);
|
|
|
|
if (std::abs(requested_z - GetZ()) > 100) {
|
|
LogNPCRoamBox("[{}] | Failed to find reasonable ground [{}]", GetCleanName(), i);
|
|
continue;
|
|
}
|
|
|
|
std::vector<float> heights = {0, 250, -250};
|
|
for (auto &h: heights) {
|
|
if (CanPathTo(requested_x, requested_y, requested_z + h)) {
|
|
LogNPCRoamBox("[{}] Found line of sight to path attempt [{}] at height [{}]", GetCleanName(), i, h);
|
|
can_path = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!can_path) {
|
|
LogNPCRoamBox("[{}] | Failed line of sight to path attempt [{}]", GetCleanName(), i);
|
|
continue;
|
|
}
|
|
|
|
m_roambox.dest_x = requested_x;
|
|
m_roambox.dest_y = requested_y;
|
|
|
|
/**
|
|
* If our roambox was configured with large distances, chances of hitting the min or max end of
|
|
* the clamp is high, this causes NPC's to gather on the border of a box, to reduce clustering
|
|
* either lower the roambox distance or the code will do a simple random between min - max when it
|
|
* hits the min or max of the clamp
|
|
*/
|
|
if (m_roambox.dest_x == m_roambox.min_x || m_roambox.dest_x == m_roambox.max_x) {
|
|
m_roambox.dest_x = static_cast<float>(zone->random.Real(m_roambox.min_x, m_roambox.max_x));
|
|
}
|
|
|
|
if (m_roambox.dest_y == m_roambox.min_y || m_roambox.dest_y == m_roambox.max_y) {
|
|
m_roambox.dest_y = static_cast<float>(zone->random.Real(m_roambox.min_y, m_roambox.max_y));
|
|
}
|
|
|
|
// If mob was not spawned in water, let's not randomly roam them into water
|
|
// if the roam box was sloppily configured
|
|
if (!GetWasSpawnedInWater()) {
|
|
m_roambox.dest_z = GetGroundZ(m_roambox.dest_x, m_roambox.dest_y);
|
|
if (zone->HasMap() && zone->HasWaterMap()) {
|
|
auto position = glm::vec3(
|
|
m_roambox.dest_x,
|
|
m_roambox.dest_y,
|
|
m_roambox.dest_z
|
|
);
|
|
|
|
// If someone brought us into water when we naturally wouldn't path there, return to spawn
|
|
if (zone->watermap->InLiquid(position) && zone->watermap->InLiquid(m_Position)) {
|
|
m_roambox.dest_x = m_SpawnPoint.x;
|
|
m_roambox.dest_y = m_SpawnPoint.y;
|
|
}
|
|
|
|
if (zone->watermap->InLiquid(position)) {
|
|
LogNPCRoamBoxDetail("[{}] | My destination is in water and I don't belong there!", GetCleanName());
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else { // Mob was in water, make sure new spot is in water also
|
|
m_roambox.dest_z = m_Position.z;
|
|
auto position = glm::vec3(
|
|
m_roambox.dest_x,
|
|
m_roambox.dest_y,
|
|
m_Position.z + 15
|
|
);
|
|
if (zone->HasWaterMap() && !zone->watermap->InLiquid(position)) {
|
|
m_roambox.dest_x = m_SpawnPoint.x;
|
|
m_roambox.dest_y = m_SpawnPoint.y;
|
|
m_roambox.dest_z = m_SpawnPoint.z;
|
|
}
|
|
}
|
|
|
|
LogNPCRoamBox(
|
|
"[{}] | Pathing to [{}] [{}] [{}]",
|
|
GetCleanName(),
|
|
m_roambox.dest_x,
|
|
m_roambox.dest_y,
|
|
m_roambox.dest_z
|
|
);
|
|
|
|
LogNPCRoamBox(
|
|
"NPC ({}) distance [{}] X (min/max) [{} / {}] Y (min/max) [{} / {}] | Dest x/y/z [{} / {} / {}]",
|
|
GetCleanName(),
|
|
m_roambox.distance,
|
|
m_roambox.min_x,
|
|
m_roambox.max_x,
|
|
m_roambox.min_y,
|
|
m_roambox.max_y,
|
|
m_roambox.dest_x,
|
|
m_roambox.dest_y,
|
|
m_roambox.dest_z
|
|
);
|
|
|
|
if (can_path) {
|
|
SetCurrentWP(EQ::WaypointStatus::RoamBoxPauseInProgress);
|
|
NavigateTo(m_roambox.dest_x, m_roambox.dest_y, m_roambox.dest_z);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// failed to find path, reset timer
|
|
int roambox_move_delay = EQ::ClampLower(GetRoamboxDelay(), GetRoamboxMinDelay());
|
|
int move_delay_max = (roambox_move_delay > 0 ? roambox_move_delay : (int) GetRoamboxMinDelay() * 4);
|
|
int random_timer = RandomTimer(
|
|
GetRoamboxMinDelay(),
|
|
move_delay_max
|
|
);
|
|
time_until_can_move = Timer::GetCurrentTime() + random_timer;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void NPC::SetTaunting(bool is_taunting) {
|
|
taunting = is_taunting;
|
|
|
|
if (IsPet() && IsPetOwnerClient()) {
|
|
GetOwner()->CastToClient()->SetPetCommandState(PET_BUTTON_TAUNT, is_taunting);
|
|
}
|
|
}
|
|
|
|
bool NPC::CanPathTo(float x, float y, float z)
|
|
{
|
|
PathfinderOptions opts;
|
|
opts.smooth_path = true;
|
|
opts.step_size = RuleR(Pathing, NavmeshStepSize);
|
|
opts.offset = GetZOffset();
|
|
opts.flags = PathingNotDisabled ^ PathingZoneLine;
|
|
|
|
bool partial = false;
|
|
bool stuck = false;
|
|
auto route = zone->pathing->FindPath(
|
|
glm::vec3(GetX(), GetY(), GetZ()),
|
|
glm::vec3(x, y, z),
|
|
partial,
|
|
stuck,
|
|
opts
|
|
);
|
|
|
|
return !route.empty();
|
|
}
|
|
|
|
void NPC::DescribeSpecialAbilities(Client* c)
|
|
{
|
|
if (!c) {
|
|
return;
|
|
}
|
|
|
|
// These abilities are simple on/off flags
|
|
static const std::vector<uint32> toggleable_special_abilities = {
|
|
SpecialAbility::TripleAttack,
|
|
SpecialAbility::QuadrupleAttack,
|
|
SpecialAbility::DualWield,
|
|
SpecialAbility::BaneAttack,
|
|
SpecialAbility::MagicalAttack,
|
|
SpecialAbility::SlowImmunity,
|
|
SpecialAbility::MesmerizeImmunity,
|
|
SpecialAbility::CharmImmunity,
|
|
SpecialAbility::StunImmunity,
|
|
SpecialAbility::SnareImmunity,
|
|
SpecialAbility::FearImmunity,
|
|
SpecialAbility::DispellImmunity,
|
|
SpecialAbility::MeleeImmunity,
|
|
SpecialAbility::MagicImmunity,
|
|
SpecialAbility::FleeingImmunity,
|
|
SpecialAbility::MeleeImmunityExceptBane,
|
|
SpecialAbility::MeleeImmunityExceptMagical,
|
|
SpecialAbility::AggroImmunity,
|
|
SpecialAbility::BeingAggroImmunity,
|
|
SpecialAbility::CastingFromRangeImmunity,
|
|
SpecialAbility::FeignDeathImmunity,
|
|
SpecialAbility::TauntImmunity,
|
|
SpecialAbility::NoBuffHealFriends,
|
|
SpecialAbility::PacifyImmunity,
|
|
SpecialAbility::DestructibleObject,
|
|
SpecialAbility::HarmFromClientImmunity,
|
|
SpecialAbility::AlwaysFlee,
|
|
SpecialAbility::AllowBeneficial,
|
|
SpecialAbility::DisableMelee,
|
|
SpecialAbility::AllowedToTank,
|
|
SpecialAbility::IgnoreRootAggroRules,
|
|
SpecialAbility::ProximityAggro,
|
|
SpecialAbility::RangedAttackImmunity,
|
|
SpecialAbility::ClientDamageImmunity,
|
|
SpecialAbility::NPCDamageImmunity,
|
|
SpecialAbility::ClientAggroImmunity,
|
|
SpecialAbility::NPCAggroImmunity,
|
|
SpecialAbility::MemoryFadeImmunity,
|
|
SpecialAbility::OpenImmunity,
|
|
SpecialAbility::AssassinateImmunity,
|
|
SpecialAbility::HeadshotImmunity,
|
|
SpecialAbility::BotAggroImmunity,
|
|
SpecialAbility::BotDamageImmunity
|
|
};
|
|
|
|
// These abilities have parameters that need to be parsed out individually
|
|
static const std::map<uint32, std::vector<std::string>> parameter_special_abilities = {
|
|
{ SpecialAbility::Summon, { "Cooldown in Milliseconds", "Health Percentage" } },
|
|
{
|
|
SpecialAbility::Enrage,
|
|
{
|
|
"Health Percentage",
|
|
"Duration in Milliseconds",
|
|
"Cooldown in Milliseconds"
|
|
}
|
|
},
|
|
{
|
|
SpecialAbility::Rampage,
|
|
{
|
|
"Chance",
|
|
"Targets",
|
|
"Flat Damage Bonus",
|
|
"Ignore Armor Percentage",
|
|
"Ignore Flat Armor Amount",
|
|
"Critical Chance",
|
|
"Flat Critical Chance Bonus"
|
|
}
|
|
},
|
|
{
|
|
SpecialAbility::AreaRampage,
|
|
{
|
|
"Targets",
|
|
"Normal Attack Damage Percentage",
|
|
"Flat Damage Bonus",
|
|
"Ignore Armor Percentage",
|
|
"Ignore Flat Armor Amount",
|
|
"Critical Chance",
|
|
"Flat Critical Chance Bonus"
|
|
}
|
|
},
|
|
{
|
|
SpecialAbility::Flurry,
|
|
{
|
|
"Attacks",
|
|
"Normal Attack Damage Percentage",
|
|
"Flat Damage Bonus",
|
|
"Ignore Armor Percentage",
|
|
"Ignore Flat Armor Amount",
|
|
"Critical Chance",
|
|
"Flat Critical Chance Bonus"
|
|
}
|
|
},
|
|
{
|
|
SpecialAbility::RangedAttack,
|
|
{
|
|
"Attacks",
|
|
"Maximum Range",
|
|
"Chance",
|
|
"Damage Percentage",
|
|
"Minimum Range"
|
|
}
|
|
},
|
|
{ SpecialAbility::TunnelVision, { "Aggro Modifier on Non-Tanks" } },
|
|
{ SpecialAbility::Leash, { "Range" } },
|
|
{ SpecialAbility::Tether, { "Range" } },
|
|
{ SpecialAbility::FleePercent, { "Health Percentage", "Chance" } },
|
|
{
|
|
SpecialAbility::NPCChaseDistance,
|
|
{
|
|
"Maximum Distance",
|
|
"Minimum Distance",
|
|
"Ignore Line of Sight"
|
|
}
|
|
},
|
|
{ SpecialAbility::CastingResistDifficulty, { "Resist Difficulty Value" } },
|
|
{
|
|
SpecialAbility::CounterAvoidDamage,
|
|
{
|
|
"Reduction Percentage for Block, Dodge, Parry, and Riposte",
|
|
"Reduction Percentage for Riposte",
|
|
"Reduction Percentage for Block",
|
|
"Reduction Percentage for Parry",
|
|
"Reduction Percentage for Dodge",
|
|
}
|
|
},
|
|
{
|
|
SpecialAbility::ModifyAvoidDamage,
|
|
{
|
|
"Addition Percentage for Block, Dodge, Parry, and Riposte",
|
|
"Addition Percentage for Riposte",
|
|
"Addition Percentage for Block",
|
|
"Addition Percentage for Parry",
|
|
"Addition Percentage for Dodge",
|
|
}
|
|
},
|
|
};
|
|
|
|
std::vector<std::string> messages = { };
|
|
|
|
for (const auto& e : toggleable_special_abilities) {
|
|
if (GetSpecialAbility(e)) {
|
|
messages.emplace_back(
|
|
fmt::format(
|
|
"{} ({})",
|
|
SpecialAbility::GetName(e),
|
|
e
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
int slot_id;
|
|
|
|
for (const auto& e : parameter_special_abilities) {
|
|
if (GetSpecialAbility(e.first)) {
|
|
slot_id = 0;
|
|
|
|
for (const auto& a : e.second) {
|
|
messages.emplace_back(
|
|
fmt::format(
|
|
"{} ({}) | {}: {}",
|
|
SpecialAbility::GetName(e.first),
|
|
e.first,
|
|
a,
|
|
GetSpecialAbilityParam(e.first, slot_id)
|
|
)
|
|
);
|
|
|
|
slot_id++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (messages.empty()) {
|
|
c->Message(
|
|
Chat::White,
|
|
fmt::format(
|
|
"{} has no special abilities.",
|
|
c->GetTargetDescription(this)
|
|
).c_str()
|
|
);
|
|
return;
|
|
}
|
|
|
|
c->Message(
|
|
Chat::White,
|
|
fmt::format(
|
|
"{} has the following special abilit{}:",
|
|
c->GetTargetDescription(this),
|
|
messages.size() != 1 ? "ies" : "y"
|
|
).c_str()
|
|
);
|
|
|
|
std::sort(
|
|
messages.begin(),
|
|
messages.end(),
|
|
[](const std::string& a, const std::string& b) {
|
|
return a < b;
|
|
}
|
|
);
|
|
|
|
for (const auto& e : messages) {
|
|
c->Message(Chat::White, e.c_str());
|
|
}
|
|
}
|
|
|
|
void NPC::DoNpcToNpcAggroScan()
|
|
{
|
|
for (auto &close_mob : GetCloseMobList(GetAggroRange())) {
|
|
Mob *mob = close_mob.second;
|
|
if (!mob) {
|
|
continue;
|
|
}
|
|
|
|
if (!mob->IsNPC()) {
|
|
continue;
|
|
}
|
|
|
|
if (CheckWillAggro(mob)) {
|
|
AddToHateList(mob);
|
|
}
|
|
}
|
|
|
|
AI_scan_area_timer->Disable();
|
|
AI_scan_area_timer->Start(
|
|
RandomTimer(RuleI(NPC, NPCToNPCAggroTimerMin), RuleI(NPC, NPCToNPCAggroTimerMax)),
|
|
false
|
|
);
|
|
}
|
|
|
|
bool NPC::FacesTarget()
|
|
{
|
|
const std::string& excluded_races_rule = RuleS(NPC, ExcludedFaceTargetRaces);
|
|
|
|
if (excluded_races_rule.empty()) {
|
|
return true;
|
|
}
|
|
|
|
const auto& v = Strings::Split(excluded_races_rule, ",");
|
|
|
|
return std::find(v.begin(), v.end(), std::to_string(GetBaseRace())) == v.end();
|
|
}
|
|
|