From 2ef959c5ed020ae18d836eb26fea4120c30555af Mon Sep 17 00:00:00 2001 From: Fryguy Date: Tue, 30 Jul 2024 18:27:47 -0400 Subject: [PATCH] [Improvement] Flee Overhaul (#4407) * Lots of flee updates primarily based on TAKPs source * Update Values to EQEmu values. * Add rule * Adjustments to fear pathing * Flee/Pathing adjustments (More TAKP code adjusted) * updates * Updates (Massaged functions from TAKP source) --------- Co-authored-by: Kinglykrab --- common/ruletypes.h | 1 + zone/aggro.cpp | 70 +++++++ zone/attack.cpp | 52 +++++- zone/client.h | 2 + zone/command.cpp | 2 + zone/command.h | 3 +- zone/entity.cpp | 30 +++ zone/entity.h | 4 +- zone/fearpath.cpp | 338 ++++++++++++++++++++++++++++------ zone/gm_commands/fleeinfo.cpp | 14 ++ zone/map.cpp | 59 ++++++ zone/map.h | 2 + zone/mob.cpp | 3 +- zone/mob.h | 7 + zone/pathfinder_interface.h | 2 +- zone/pathfinder_nav_mesh.cpp | 4 +- zone/pathfinder_nav_mesh.h | 2 +- zone/pathfinder_null.cpp | 2 +- zone/pathfinder_null.h | 2 +- zone/pathfinder_waypoint.cpp | 2 +- zone/pathfinder_waypoint.h | 2 +- zone/zone.cpp | 16 ++ zone/zone.h | 1 + 23 files changed, 543 insertions(+), 77 deletions(-) create mode 100644 zone/gm_commands/fleeinfo.cpp diff --git a/common/ruletypes.h b/common/ruletypes.h index 3964906b9..8fe67be66 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -699,6 +699,7 @@ RULE_BOOL(Aggro, UndeadAlwaysAggro, true, "should undead always aggro?") RULE_INT(Aggro, BardAggroCap, 40, "per song bard aggro cap.") RULE_INT(Aggro, InitialAggroBonus, 100, "Initial Aggro Bonus, Default: 100") RULE_INT(Aggro, InitialPetAggroBonus, 100, "Initial Pet Aggro Bonus, Default 100") +RULE_STRING(Aggro, ExcludedFleeAllyFactionIDs, "0|5013|5014|5023|5032", "Common Faction IDs that are excluded from faction checks in EntityList::FleeAllyCount") RULE_CATEGORY_END() RULE_CATEGORY(TaskSystem) diff --git a/zone/aggro.cpp b/zone/aggro.cpp index 4ddfb759a..1bc7efa76 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -582,6 +582,76 @@ bool Mob::CheckWillAggro(Mob *mob) { return false; } +int EntityList::FleeAllyCount(Mob* attacker, Mob* skipped) +{ + // Return a list of how many NPCs of the same faction or race are within aggro range of the given exclude Mob. + if (!attacker) { + return 0; + } + + int count = 0; + + for (const auto& e : npc_list) { + NPC* n = e.second; + if (!n || n == skipped) { + continue; + } + + float aggro_range = n->GetAggroRange(); + const float assist_range = n->GetAssistRange(); + + if (assist_range > aggro_range) { + aggro_range = assist_range; + } + + // Square it because we will be using DistNoRoot + aggro_range *= aggro_range; + + if (DistanceSquared(n->GetPosition(), skipped->GetPosition()) > aggro_range) { + continue; + } + + const auto& excluded = Strings::Split(RuleS(Aggro, ExcludedFleeAllyFactionIDs)); + + const auto& f = std::find_if( + excluded.begin(), + excluded.end(), + [&](std::string x) { + return Strings::ToUnsignedInt(x) == skipped->GetPrimaryFaction(); + } + ); + + const bool is_excluded = f != excluded.end(); + + // If exclude doesn't have a faction, check for buddies based on race. + // Also exclude common factions such as noob monsters, indifferent, kos, kos animal + if (!is_excluded) { + if (n->GetPrimaryFaction() != skipped->GetPrimaryFaction()) { + continue; + } + } else { + if (n->GetBaseRace() != skipped->GetBaseRace() || n->IsCharmedPet()) { + continue; + } + } + + LogFleeDetail( + "[{}] on faction [{}] with aggro_range [{}] is at [{}], [{}], [{}] and will count as an ally for [{}]", + n->GetName(), + n->GetPrimaryFaction(), + aggro_range, + n->GetX(), + n->GetY(), + n->GetZ(), + skipped->GetName() + ); + + ++count; + } + + return count; +} + int EntityList::GetHatedCount(Mob *attacker, Mob *exclude, bool inc_gray_con) { // Return a list of how many non-feared, non-mezzed, non-green mobs, within aggro range, hate *attacker diff --git a/zone/attack.cpp b/zone/attack.cpp index 025ab4425..d404998cc 100644 --- a/zone/attack.cpp +++ b/zone/attack.cpp @@ -2186,6 +2186,19 @@ bool Client::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::Skil return true; } +bool Client::CheckIfAlreadyDead() +{ + if (!ClientFinishedLoading()) { + return false; + } + + if (dead) { + return false; //cant die more than once... + } + + return true; +} + //SYNC WITH: tune.cpp, mob.h TuneNPCAttack bool NPC::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, bool IsFromSpell, ExtraAttackOptions *opts) { @@ -2460,11 +2473,6 @@ void NPC::Damage(Mob* other, int64 damage, uint16 spell_id, EQ::skills::SkillTyp //do a majority of the work... CommonDamage(other, damage, spell_id, attack_skill, avoidable, buffslot, iBuffTic, special); - - if (damage > 0) { - //see if we are gunna start fleeing - if (!IsPet()) CheckFlee(); - } } bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillType attack_skill, KilledByTypes killed_by, bool is_buff_tic) @@ -4132,6 +4140,7 @@ void Mob::CommonDamage(Mob* attacker, int64 &damage, const uint16 spell_id, cons AddToHateList(attacker, 0, damage, true, false, iBuffTic, spell_id); } + bool died = false; if (damage > 0) { //if there is some damage being done and theres an attacker involved int previous_hp_ratio = GetHPRatio(); @@ -4256,6 +4265,8 @@ void Mob::CommonDamage(Mob* attacker, int64 &damage, const uint16 spell_id, cons } //final damage has been determined. + int old_hp_ratio = (int)GetHPRatio(); + SetHP(int64(GetHP() - damage)); const auto has_bot_given_event = parse->BotHasQuestSub(EVENT_DAMAGE_GIVEN); @@ -4355,11 +4366,21 @@ void Mob::CommonDamage(Mob* attacker, int64 &damage, const uint16 spell_id, cons if (HasDied()) { bool IsSaved = false; - if (TryDivineSave()) + if (TryDivineSave()) { IsSaved = true; + } if (!IsSaved && !TrySpellOnDeath()) { - SetHP(-500); + if (IsNPC()) { + died = !CastToNPC()->GetDepop(); + } else if (IsClient()) { + died = CastToClient()->CheckIfAlreadyDead(); + } + + if (died) { + SetHP(-500); + } + // killedByType is clarified in Client::Death if we are client. if (Death(attacker, damage, spell_id, skill_used, KilledByTypes::Killed_NPC, iBuffTic)) { return; @@ -4529,8 +4550,21 @@ void Mob::CommonDamage(Mob* attacker, int64 &damage, const uint16 spell_id, cons } //send an HP update if we are hurt - if (GetHP() < GetMaxHP()) { - SendHPUpdate(); // the OP_Damage actually updates the client in these cases, so we skip the HP update for them + if(GetHP() < GetMaxHP()) + { + // Don't send a HP update for melee damage unless we've damaged ourself. + if (IsNPC()) { + int cur_hp_ratio = (int)GetHPRatio(); + if (cur_hp_ratio != old_hp_ratio) { + SendHPUpdate(true); + } + } else if (!iBuffTic || died) { // Let regen handle buff tics unless this tic killed us. + SendHPUpdate(true); + } + + if (!died && IsNPC()) { + CheckFlee(); + } } } //end `if damage was done` diff --git a/zone/client.h b/zone/client.h index 37f60194c..5c5694e7a 100644 --- a/zone/client.h +++ b/zone/client.h @@ -278,6 +278,8 @@ public: std::vector GetRaidOrGroupOrSelf(bool clients_only = false); + bool CheckIfAlreadyDead(); + void AI_Init(); void AI_Start(uint32 iMoveDelay = 0); void AI_Stop(); diff --git a/zone/command.cpp b/zone/command.cpp index cc6225a99..b701f2bd2 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -134,6 +134,7 @@ int command_init(void) command_add("fish", "Fish for an item", AccountStatus::QuestTroupe, command_fish) || command_add("fixmob", "[race|gender|texture|helm|face|hair|haircolor|beard|beardcolor|heritage|tattoo|detail] [next|prev] - Manipulate appearance of your target", AccountStatus::QuestTroupe, command_fixmob) || command_add("flagedit", "Edit zone flags on your target. Use #flagedit help for more info.", AccountStatus::GMAdmin, command_flagedit) || + command_add("fleeinfo", "- Gives info about whether a NPC will flee or not, using the command issuer as top hate.", AccountStatus::QuestTroupe, command_fleeinfo) || command_add("forage", "Forage an item", AccountStatus::QuestTroupe, command_forage) || command_add("gearup", "Developer tool to quickly equip yourself or your target", AccountStatus::GMMgmt, command_gearup) || command_add("giveitem", "[itemid] [charges] - Summon an item onto your target's cursor. Charges are optional.", AccountStatus::GMMgmt, command_giveitem) || @@ -829,6 +830,7 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/fish.cpp" #include "gm_commands/fixmob.cpp" #include "gm_commands/flagedit.cpp" +#include "gm_commands/fleeinfo.cpp" #include "gm_commands/forage.cpp" #include "gm_commands/gearup.cpp" #include "gm_commands/giveitem.cpp" diff --git a/zone/command.h b/zone/command.h index ae8bf30c6..d59fdf9a5 100644 --- a/zone/command.h +++ b/zone/command.h @@ -87,6 +87,7 @@ void command_find(Client *c, const Seperator *sep); void command_fish(Client* c, const Seperator* sep); void command_fixmob(Client *c, const Seperator *sep); void command_flagedit(Client *c, const Seperator *sep); +void command_fleeinfo(Client *c, const Seperator *sep); void command_forage(Client* c, const Seperator* sep); void command_gearup(Client *c, const Seperator *sep); void command_giveitem(Client *c, const Seperator *sep); @@ -136,7 +137,7 @@ void command_nukebuffs(Client *c, const Seperator *sep); void command_nukeitem(Client *c, const Seperator *sep); void command_object(Client *c, const Seperator *sep); void command_oocmute(Client *c, const Seperator *sep); -void command_parcels(Client *c, const Seperator *sep); +void command_parcels(Client *c, const Seperator *sep); void command_path(Client *c, const Seperator *sep); void command_peqzone(Client *c, const Seperator *sep); void command_petitems(Client *c, const Seperator *sep); diff --git a/zone/entity.cpp b/zone/entity.cpp index b0a52d611..202c9025e 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -4483,6 +4483,36 @@ void EntityList::QuestJournalledSayClose( delete outapp; } +bool Entity::CheckCoordLosNoZLeaps(float cur_x, float cur_y, float cur_z, + float trg_x, float trg_y, float trg_z, float perwalk) +{ + if (zone->zonemap == nullptr) { + return true; + } + + glm::vec3 myloc; + glm::vec3 oloc; + glm::vec3 hit; + + myloc.x = cur_x; + myloc.y = cur_y; + myloc.z = cur_z+5; + + oloc.x = trg_x; + oloc.y = trg_y; + oloc.z = trg_z+5; + + if (myloc.x == oloc.x && myloc.y == oloc.y && myloc.z == oloc.z) { + return true; + } + + if (!zone->zonemap->LineIntersectsZoneNoZLeaps(myloc,oloc,perwalk,&hit)) { + return true; + } + + return false; +} + Corpse *EntityList::GetClosestCorpse(Mob *sender, const char *Name) { if (!sender) diff --git a/zone/entity.h b/zone/entity.h index f987eebbb..47fb00c9a 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -116,6 +116,7 @@ public: inline const time_t& GetSpawnTimeStamp() const { return spawn_timestamp; } virtual const char* GetName() { return ""; } + bool CheckCoordLosNoZLeaps(float cur_x, float cur_y, float cur_z, float trg_x, float trg_y, float trg_z, float perwalk=1); Bot* CastToBot(); const Bot* CastToBot() const; @@ -503,9 +504,10 @@ public: Mob* GetTargetForMez(Mob* caster); uint32 CheckNPCsClose(Mob *center); + int FleeAllyCount(Mob* attacker, Mob* skipped); Corpse* GetClosestCorpse(Mob* sender, const char *Name); void TryWakeTheDead(Mob* sender, Mob* target, int32 spell_id, uint32 max_distance, uint32 duration, uint32 amount_pets); - NPC* GetClosestBanker(Mob* sender, uint32 &distance); + NPC* GetClosestBanker(Mob* sender, uint32 &distance); void CameraEffect(uint32 duration, float intensity); Mob* GetClosestMobByBodyType(Mob* sender, uint8 BodyType, bool skip_client_pets=false); void ForceGroupUpdate(uint32 gid); diff --git a/zone/fearpath.cpp b/zone/fearpath.cpp index a1d9df32d..e7fcd458b 100644 --- a/zone/fearpath.cpp +++ b/zone/fearpath.cpp @@ -19,6 +19,7 @@ #include "../common/rulesys.h" #include "map.h" +#include "water_map.h" #include "zone.h" #ifdef _WINDOWS @@ -29,17 +30,52 @@ extern Zone* zone; #define FEAR_PATHING_DEBUG +int Mob::GetFleeRatio(Mob* other) +{ + int flee_ratio = GetSpecialAbility(SpecialAbility::FleePercent); // if a special SpecialAbility::FleePercent exists + Mob *hate_top = GetHateTop(); + + if (other != nullptr) { + hate_top = other; + } + + if (!hate_top) { + return 0; + } + + // If no special flee_percent check for Gray or Other con rates + if (flee_ratio == 0) { + flee_ratio = RuleI(Combat, FleeHPRatio); + if (GetLevelCon(hate_top->GetLevel(), GetLevel()) == ConsiderColor::Gray && RuleB(Combat, FleeGray) && + GetLevel() <= RuleI(Combat, FleeGrayMaxLevel)) { + flee_ratio = RuleI(Combat, FleeGrayHPRatio); + LogFlee("Mob [{}] using combat flee gray flee_ratio [{}]", GetCleanName(), flee_ratio); + } + } + + return flee_ratio; +} + //this is called whenever we are damaged to process possible fleeing void Mob::CheckFlee() { - - // if mob is dead why would you run? - if (GetHP() == 0) { + if (IsPet() || IsCasting() || GetHP() == 0 || GetBodyType() == BodyType::Undead || (IsNPC() && CastToNPC()->IsUnderwaterOnly())) { return; } - // if were already fleeing, don't need to check more... + //if were already fleeing, we only need to check speed. Speed changes will trigger pathing updates. if (flee_mode && currently_fleeing) { + int flee_speed = GetFearSpeed(); + if (flee_speed < 1) { + flee_speed = 0; + } + + SetRunAnimSpeed(flee_speed); + + if (IsMoving() && flee_speed < 1) { + StopNavigation(); + } + return; } @@ -49,38 +85,17 @@ void Mob::CheckFlee() return; } - // Undead do not flee - if (GetBodyType() == BodyType::Undead) { - return; - } - - // Check if Flee Timer is cleared - if (!flee_timer.Check()) { - return; - } - int hp_ratio = GetIntHPRatio(); - int flee_ratio = GetSpecialAbility(SpecialAbility::FleePercent); // if a special SpecialAbility::FleePercent exists + int flee_ratio = GetFleeRatio(); Mob *hate_top = GetHateTop(); - LogFlee("Mob [{}] hp_ratio [{}] flee_ratio [{}]", GetCleanName(), hp_ratio, flee_ratio); - // Sanity Check for race conditions - if (hate_top == nullptr) { + if(!hate_top) { + //this should never happen... + StartFleeing(); return; } - // If no special flee_percent check for Gray or Other con rates - if (GetLevelCon(hate_top->GetLevel(), GetLevel()) == ConsiderColor::Gray && flee_ratio == 0 && RuleB(Combat, FleeGray) && - GetLevel() <= RuleI(Combat, FleeGrayMaxLevel)) { - flee_ratio = RuleI(Combat, FleeGrayHPRatio); - LogFlee("Mob [{}] using combat flee gray hp_ratio [{}] flee_ratio [{}]", GetCleanName(), hp_ratio, flee_ratio); - } - else if (flee_ratio == 0) { - flee_ratio = RuleI(Combat, FleeHPRatio); - LogFlee("Mob [{}] using combat flee hp_ratio [{}] flee_ratio [{}]", GetCleanName(), hp_ratio, flee_ratio); - } - bool mob_has_low_enough_health_to_flee = hp_ratio >= flee_ratio; if (mob_has_low_enough_health_to_flee) { LogFlee( @@ -141,7 +156,7 @@ void Mob::CheckFlee() // if FleeIfNotAlone is true, we skip alone check // roll chance if (GetSpecialAbility(SpecialAbility::AlwaysFlee) || - ((RuleB(Combat, FleeIfNotAlone) || entity_list.GetHatedCount(hate_top, this, true) == 0) && + ((RuleB(Combat, FleeIfNotAlone) || entity_list.FleeAllyCount(hate_top, this) == 0) && zone->random.Roll(flee_chance))) { LogFlee( @@ -158,9 +173,63 @@ void Mob::CheckFlee() } } +void Mob::StopFleeing() +{ + if (!flee_mode) { + return; + } + + flee_mode = false; + + //see if we are legitimately feared or blind now + if (!spellbonuses.IsFeared && !IsBlind()) { + currently_fleeing = false; + StopNavigation(); + } +} + +void Mob::FleeInfo(Mob* client) +{ + float other_ratio = client->GetHPRatio(); + bool wontflee = false; + std::string reason; + std::string flee; + + int allycount = entity_list.FleeAllyCount(client, this); + + if (flee_mode && currently_fleeing) { + wontflee = true; + reason = "NPC is already fleeing!"; + } else if (GetSpecialAbility(SpecialAbility::FleeingImmunity)) { + wontflee = true; + reason = "NPC is immune to fleeing."; + } else if (other_ratio < 20) { + wontflee = true; + reason = "Player has low health."; + } else if (GetSpecialAbility(SpecialAbility::AlwaysFlee)) { + flee = "NPC has ALWAYS_FLEE set."; + } else if (RuleB(Combat, FleeIfNotAlone) || (!RuleB(Combat, FleeIfNotAlone) && allycount == 0)) { + flee = "NPC has no allies nearby or the rule to flee when not alone is enabled."; + } else { + wontflee = true; + reason = "NPC likely has allies nearby."; + } + + + if (!wontflee) { + client->Message(Chat::Green, "%s will flee at %d percent because %s", GetName(), GetFleeRatio(client), flee.c_str()); + } else { + client->Message(Chat::Red, "%s will not flee because %s", GetName(), reason.c_str()); + } + + client->Message(Chat::Default, "NPC ally count %d", allycount); +} void Mob::ProcessFlee() { + if (!flee_mode) { + return; + } //Stop fleeing if effect is applied after they start to run. //When ImmuneToFlee effect fades it will turn fear back on and check if it can still flee. @@ -170,46 +239,201 @@ void Mob::ProcessFlee() return; } - int hpratio = GetIntHPRatio(); - int fleeratio = GetSpecialAbility(SpecialAbility::FleePercent); // if a special SpecialAbility::FleePercent exists Mob *hate_top = GetHateTop(); + bool dying = GetIntHPRatio() < GetFleeRatio(); - // If no special flee_percent check for Gray or Other con rates - if(hate_top != nullptr && GetLevelCon(hate_top->GetLevel(), GetLevel()) == ConsiderColor::Gray && fleeratio == 0 && RuleB(Combat, FleeGray)) { - fleeratio = RuleI(Combat, FleeGrayHPRatio); - } else if(fleeratio == 0) { - fleeratio = RuleI(Combat, FleeHPRatio ); + // We have stopped fleeing for an unknown reason (couldn't find a node is possible) restart. + if (flee_mode && !currently_fleeing) { + if(dying) { + StartFleeing(); + } } - // Mob is still too low. Keep Running - if(hpratio < fleeratio) { + //see if we are still dying, if so, do nothing + if (dying) { return; } - //we are not dying anymore... see what we do next - - flee_mode = false; - - //see if we are legitimately feared or blind now - if (!spellbonuses.IsFeared && !spellbonuses.IsBlind) { - //not feared or blind... were done... - currently_fleeing = false; - return; - } + //we are not dying anymore, check to make sure we're not blind or feared and cancel flee. + StopFleeing(); } void Mob::CalculateNewFearpoint() { - if (RuleB(Pathing, Fear) && zone->pathing) { - auto Node = zone->pathing->GetRandomLocation(glm::vec3(GetX(), GetY(), GetZ())); - if (Node.x != 0.0f || Node.y != 0.0f || Node.z != 0.0f) { - m_FearWalkTarget = Node; - currently_fleeing = true; - - return; + // blind waypoint logic isn't the same as fear's. Has a chance to run toward the player + // chance is very high if the player is moving, otherwise it's low + if (IsBlind() && !IsFeared() && GetTarget()) { + int roll = 20; + if (GetTarget()->GetCurrentSpeed() > 0.1f || (GetTarget()->IsClient() && GetTarget()->animation != 0)) { + roll = 80; } - LogPathing("No path found to selected node during CalculateNewFearpoint."); + if (zone->random.Roll(roll)) { + m_FearWalkTarget = glm::vec3(GetTarget()->GetPosition()); + currently_fleeing = true; + return; + } } + + if (RuleB(Pathing, Fear) && zone->pathing) { + glm::vec3 Node; + int flags = PathingNotDisabled ^ PathingZoneLine; + + if (IsNPC() && CastToNPC()->IsUnderwaterOnly() && !zone->IsWaterZone(GetZOffset())) { + Node = glm::vec3(0.0f); + } else { + Node = zone->pathing->GetRandomLocation(glm::vec3(GetX(), GetY(), GetZOffset()), flags); + } + + if (Node.x != 0.0f || Node.y != 0.0f || Node.z != 0.0f) { + Node.z = GetFixedZ(Node); + PathfinderOptions opts; + opts.smooth_path = true; + opts.step_size = RuleR(Pathing, NavmeshStepSize); + opts.offset = GetZOffset(); + opts.flags = flags; + auto partial = false; + auto stuck = false; + auto route = zone->pathing->FindPath( + glm::vec3(GetX(), GetY(), GetZOffset()), + glm::vec3(Node.x, Node.y, Node.z), + partial, + stuck, + opts + ); + glm::vec3 last_good_loc = Node; + int route_size = route.size(); + int route_count = 0; + bool have_los = true; + + if (route_size == 2) { + // FindPath() often fails to compute a route in some places, so to prevent running through walls we need to check LOS on all 2 node routes + // size 2 route usually means FindPath() bugged out. sometimes it returns locs outside the geometry + if (CheckLosFN(Node.x, Node.y, Node.z, 6.0)) { + LogPathingDetail("Direct route to fearpoint [{}], [{}], [{}] calculated for [{}]", last_good_loc.x, last_good_loc.y, last_good_loc.z, GetName()); + m_FearWalkTarget = last_good_loc; + currently_fleeing = true; + return; + } else { + LogPathingDetail("FindRoute() returned single hop route to destination without LOS: [{}], [{}], [{}] for [{}]", last_good_loc.x, last_good_loc.y, last_good_loc.z, GetName()); + } + // use fallback logic if LOS fails + } else if (!stuck) { + // check route for LOS failures to prevent mobs ending up outside of playable area + // only checking the last few hops because LOS will often fail in a valid route which can result in mobs getting undesirably trapped + auto iter = route.begin(); + glm::vec3 previous_pos(GetX(), GetY(), GetZOffset()); + while (iter != route.end() && have_los == true) { + auto ¤t_node = (*iter); + iter++; + route_count++; + + if (iter == route.end()) { + continue; + } + + previous_pos = current_node.pos; + auto &next_node = (*iter); + + if (next_node.teleport) { + continue; + } + + if ((route_size - route_count) < 5 && !zone->zonemap->CheckLoS(previous_pos, next_node.pos)) { + have_los = false; + break; + } else { + last_good_loc = next_node.pos; + } + } + + if (have_los || route_count > 2) { + if (have_los) { + LogPathingDetail("Route to fearpoint [{}], [{}], [{}] calculated for [{}]; route size: [{}]", last_good_loc.x, last_good_loc.y, last_good_loc.z, GetName(), route_size); + } else { + LogPathingDetail("Using truncated route to fearpoint [{}], [{}], [{}] for [{}]; node count: [{}]; route size [{}]", last_good_loc.x, last_good_loc.y, last_good_loc.z, GetName(), route_count, route_size); + } + + m_FearWalkTarget = last_good_loc; + currently_fleeing = true; + return; + } + } + } + } + + // fallback logic if pathing system can't be used + bool inliquid = zone->HasWaterMap() && zone->watermap->InLiquid(glm::vec3(GetPosition())) || zone->IsWaterZone(GetZ()); + bool stay_inliquid = (inliquid && IsNPC() && CastToNPC()->IsUnderwaterOnly()); + bool levitating = IsClient() && (FindType(SE_Levitate) || flymode != GravityBehavior::Ground); + bool open_outdoor_zone = !zone->CanCastOutdoor() && !zone->IsCity(); + + int loop = 0; + float ranx, rany, ranz; + currently_fleeing = false; + glm::vec3 myloc(GetX(), GetY(), GetZ()); + glm::vec3 myceil = myloc; + float ceil = zone->zonemap->FindCeiling(myloc, &myceil); + + if (ceil != BEST_Z_INVALID) { + ceil -= 1.0f; + } + + while (loop < 100) { //Max 100 tries + int ran = 250 - (loop * 2); + loop++; + + if (open_outdoor_zone && loop < 20) { // try a distant loc first; other way will likely pick a close loc + ranx = zone->random.Int(0, ran); + rany = zone->random.Int(0, ran); + if (ranx + rany < 200) { + continue; + } + + ranx = GetX() + (zone->random.Int(0, 1) == 1 ? ranx : -ranx); + rany = GetY() + (zone->random.Int(0, 1) == 1 ? rany : -rany); + } else { + ranx = GetX() + zone->random.Int(0, ran - 1) - zone->random.Int(0, ran - 1); + rany = GetY() + zone->random.Int(0, ran - 1) - zone->random.Int(0, ran - 1); + } + + ranz = BEST_Z_INVALID; + glm::vec3 newloc(ranx, rany, ceil != BEST_Z_INVALID ? ceil : GetZ()); + + if (stay_inliquid || levitating || (loop > 50 && inliquid)) { + if (zone->zonemap->CheckLoS(myloc, newloc)) { + ranz = GetZ(); + currently_fleeing = true; + break; + } + } else { + if (ceil != BEST_Z_INVALID) { + ranz = zone->zonemap->FindGround(newloc, &myceil); + } else { + ranz = zone->zonemap->FindBestZ(newloc, &myceil); + } + + if (ranz != BEST_Z_INVALID) { + ranz = SetBestZ(ranz); + } + } + + if (ranz == BEST_Z_INVALID) { + continue; + } + + float fdist = ranz - GetZ(); + if (fdist >= -50 && fdist <= 50 && CheckCoordLosNoZLeaps(GetX(), GetY(), GetZ(), ranx, rany, ranz)) { + currently_fleeing = true; + break; + } + } + + if (currently_fleeing) { + m_FearWalkTarget = glm::vec3(ranx, rany, ranz); + LogPathingDetail("Non-pathed fearpoint [{}], [{}], [{}] selected for [{}]", ranx, rany, ranz, GetName()); + } + + return; } diff --git a/zone/gm_commands/fleeinfo.cpp b/zone/gm_commands/fleeinfo.cpp new file mode 100644 index 000000000..1461bfd3c --- /dev/null +++ b/zone/gm_commands/fleeinfo.cpp @@ -0,0 +1,14 @@ +#include "../client.h" + +void command_fleeinfo(Client *c, const Seperator *sep) +{ + if (c->GetTarget() && c->GetTarget()->IsNPC()) { + Mob* client = entity_list.GetMob(c->GetID()); + if (client) { + c->GetTarget()->FleeInfo(client); + } + } else { + c->Message(Chat::Red, "Please target a NPC to use this command on."); + } + +} diff --git a/zone/map.cpp b/zone/map.cpp index cda7d1ca1..3ed0e585b 100644 --- a/zone/map.cpp +++ b/zone/map.cpp @@ -2,6 +2,7 @@ #include "../common/misc_functions.h" #include "../common/compression.h" +#include "client.h" #include "map.h" #include "raycast_mesh.h" #include "zone.h" @@ -95,6 +96,64 @@ float Map::FindClosestZ(glm::vec3 &start, glm::vec3 *result) const { return ClosestZ; } +float Map::FindCeiling(glm::vec3 &start, glm::vec3 *result) const { + // Unlike FindBestZ, this method finds the closest Z above point. + + if (!imp) { + return false; + } + + glm::vec3 tmp; + if (!result) { + result = &tmp; + } + + glm::vec3 from(start.x, start.y, start.z); + glm::vec3 to(start.x, start.y, -BEST_Z_INVALID); + float hit_distance; + bool hit = false; + + // Find nearest Z above us + hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance); + if (hit) { + return result->z; + } + + return BEST_Z_INVALID; +} + +float Map::FindGround(glm::vec3 &start, glm::vec3 *result) const { + // Unlike FindBestZ, this method finds the closest Z below point. + + if (!imp) { + return false; + } + + glm::vec3 tmp; + + if (!result) { + result = &tmp; + } + + glm::vec3 from(start.x, start.y, start.z); + glm::vec3 to(start.x, start.y, BEST_Z_INVALID); + float hit_distance; + bool hit = false; + + // Find nearest Z below us + hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance); + + if (hit && zone->newzone_data.underworld != 0.0f && result->z < zone->newzone_data.underworld) { + hit = false; + } + + if (hit) { + return result->z; + } + + return BEST_Z_INVALID; +} + bool Map::LineIntersectsZone(glm::vec3 start, glm::vec3 end, float step, glm::vec3 *result) const { if(!imp) return false; diff --git a/zone/map.h b/zone/map.h index eefb43f2d..20fad6e95 100644 --- a/zone/map.h +++ b/zone/map.h @@ -39,6 +39,8 @@ public: float FindBestZ(glm::vec3 &start, glm::vec3 *result) const; float FindClosestZ(glm::vec3 &start, glm::vec3 *result) const; + float FindCeiling(glm::vec3 &start, glm::vec3 *result) const; + float FindGround(glm::vec3 &start, glm::vec3 *result) const; bool LineIntersectsZone(glm::vec3 start, glm::vec3 end, float step, glm::vec3 *result) const; bool LineIntersectsZoneNoZLeaps(glm::vec3 start, glm::vec3 end, float step_mag, glm::vec3 *result) const; bool CheckLoS(glm::vec3 myloc, glm::vec3 oloc) const; diff --git a/zone/mob.cpp b/zone/mob.cpp index c9a8e0292..573425a1a 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -877,8 +877,9 @@ int Mob::_GetRunSpeed() const { int Mob::_GetFearSpeed() const { - if (IsRooted() || IsStunned() || IsMezzed()) + if (IsRooted() || IsStunned() || IsMezzed()) { return 0; + } //float speed_mod = fearspeed; int speed_mod = GetBaseFearSpeed(); diff --git a/zone/mob.h b/zone/mob.h index 86b8db5d5..e8a5c33aa 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -678,6 +678,7 @@ public: inline const float GetRelativeHeading() const { return m_RelativePosition.w; } inline const float GetSize() const { return size; } inline const float GetBaseSize() const { return base_size; } + inline const float SetBestZ(float z_coord) const { return z_coord + GetZOffset(); } inline const GravityBehavior GetFlyMode() const { return flymode; } bool IsBoat() const; // Checks races - used on mob instantiation bool GetIsBoat() const { return is_boat; } // Set on instantiation for speed @@ -717,6 +718,7 @@ public: float GetMovespeed() const { return IsRunning() ? GetRunspeed() : GetWalkspeed(); } bool IsRunning() const { return m_is_running; } void SetRunning(bool val) { m_is_running = val; } + float GetCurrentSpeed() { return current_speed; } virtual void GMMove(float x, float y, float z, float heading = 0.01, bool save_guard_spot = true); virtual void GMMove(const glm::vec4 &position, bool save_guard_spot = true); void SetDelta(const glm::vec4& delta); @@ -1074,6 +1076,7 @@ public: inline virtual bool HasOwner() { if (!GetOwnerID()){ return false; } return entity_list.GetMob(GetOwnerID()) != 0; } inline virtual bool IsPet() { return HasOwner() && !IsMerc(); } bool HasPet() const; + virtual bool IsCharmedPet() { return IsPet() && IsCharmed(); } inline bool HasTempPetsActive() const { return(hasTempPet); } inline void SetTempPetsActive(bool i) { hasTempPet = i; } inline int16 GetTempPetCount() const { return count_TempPet; } @@ -1210,8 +1213,12 @@ public: int GetFearSpeed() { return _GetFearSpeed(); } bool IsFeared() { return (spellbonuses.IsFeared || flee_mode); } // This returns true if the mob is feared or fleeing due to low HP inline void StartFleeing() { flee_mode = true; CalculateNewFearpoint(); } + void StopFleeing(); + inline bool IsFleeing() { return flee_mode; } void ProcessFlee(); void CheckFlee(); + void FleeInfo(Mob* client); + int GetFleeRatio(Mob* other = nullptr); inline bool IsBlind() { return spellbonuses.IsBlind; } inline bool CheckAggro(Mob* other) {return hate_list.IsEntOnHateList(other);} diff --git a/zone/pathfinder_interface.h b/zone/pathfinder_interface.h index 4984efe14..d69dbac74 100644 --- a/zone/pathfinder_interface.h +++ b/zone/pathfinder_interface.h @@ -75,7 +75,7 @@ public: virtual IPath FindRoute(const glm::vec3 &start, const glm::vec3 &end, bool &partial, bool &stuck, int flags = PathingNotDisabled) = 0; virtual IPath FindPath(const glm::vec3 &start, const glm::vec3 &end, bool &partial, bool &stuck, const PathfinderOptions& opts) = 0; - virtual glm::vec3 GetRandomLocation(const glm::vec3 &start) = 0; + virtual glm::vec3 GetRandomLocation(const glm::vec3 &start, int flags = PathingNotDisabled) = 0; virtual void DebugCommand(Client *c, const Seperator *sep) = 0; static IPathfinder *Load(const std::string &zone); diff --git a/zone/pathfinder_nav_mesh.cpp b/zone/pathfinder_nav_mesh.cpp index 9877b8323..22dd12249 100644 --- a/zone/pathfinder_nav_mesh.cpp +++ b/zone/pathfinder_nav_mesh.cpp @@ -300,7 +300,7 @@ IPathfinder::IPath PathfinderNavmesh::FindPath(const glm::vec3 &start, const glm return IPath(); } -glm::vec3 PathfinderNavmesh::GetRandomLocation(const glm::vec3 &start) +glm::vec3 PathfinderNavmesh::GetRandomLocation(const glm::vec3 &start, int flags) { if (start.x == 0.0f && start.y == 0.0) return glm::vec3(0.f); @@ -315,7 +315,7 @@ glm::vec3 PathfinderNavmesh::GetRandomLocation(const glm::vec3 &start) } dtQueryFilter filter; - filter.setIncludeFlags(65535U ^ 2048); + filter.setIncludeFlags(flags); filter.setAreaCost(0, 1.0f); //Normal filter.setAreaCost(1, 3.0f); //Water filter.setAreaCost(2, 5.0f); //Lava diff --git a/zone/pathfinder_nav_mesh.h b/zone/pathfinder_nav_mesh.h index 565a4a22a..9f7bf4120 100644 --- a/zone/pathfinder_nav_mesh.h +++ b/zone/pathfinder_nav_mesh.h @@ -12,7 +12,7 @@ public: virtual IPath FindRoute(const glm::vec3 &start, const glm::vec3 &end, bool &partial, bool &stuck, int flags = PathingNotDisabled); virtual IPath FindPath(const glm::vec3 &start, const glm::vec3 &end, bool &partial, bool &stuck, const PathfinderOptions& opts); - virtual glm::vec3 GetRandomLocation(const glm::vec3 &start); + virtual glm::vec3 GetRandomLocation(const glm::vec3 &start, int flags = PathingNotDisabled); virtual void DebugCommand(Client *c, const Seperator *sep); private: diff --git a/zone/pathfinder_null.cpp b/zone/pathfinder_null.cpp index 35d0a131e..7ec274653 100644 --- a/zone/pathfinder_null.cpp +++ b/zone/pathfinder_null.cpp @@ -20,7 +20,7 @@ IPathfinder::IPath PathfinderNull::FindPath(const glm::vec3 &start, const glm::v return ret; } -glm::vec3 PathfinderNull::GetRandomLocation(const glm::vec3 &start) +glm::vec3 PathfinderNull::GetRandomLocation(const glm::vec3 &start, int flags) { return glm::vec3(0.0f); } diff --git a/zone/pathfinder_null.h b/zone/pathfinder_null.h index 8c750e92a..146dde2cc 100644 --- a/zone/pathfinder_null.h +++ b/zone/pathfinder_null.h @@ -10,6 +10,6 @@ public: virtual IPath FindRoute(const glm::vec3 &start, const glm::vec3 &end, bool &partial, bool &stuck, int flags = PathingNotDisabled); virtual IPath FindPath(const glm::vec3 &start, const glm::vec3 &end, bool &partial, bool &stuck, const PathfinderOptions& opts); - virtual glm::vec3 GetRandomLocation(const glm::vec3 &start); + virtual glm::vec3 GetRandomLocation(const glm::vec3 &start, int flags = PathingNotDisabled); virtual void DebugCommand(Client *c, const Seperator *sep) { } }; diff --git a/zone/pathfinder_waypoint.cpp b/zone/pathfinder_waypoint.cpp index 26fb07a70..a13c3ac74 100644 --- a/zone/pathfinder_waypoint.cpp +++ b/zone/pathfinder_waypoint.cpp @@ -181,7 +181,7 @@ IPathfinder::IPath PathfinderWaypoint::FindRoute(const glm::vec3 &start, const g return IPath(); } -glm::vec3 PathfinderWaypoint::GetRandomLocation(const glm::vec3 &start) +glm::vec3 PathfinderWaypoint::GetRandomLocation(const glm::vec3 &start, int flags) { if (m_impl->Nodes.size() > 0) { auto idx = zone->random.Int(0, (int)m_impl->Nodes.size() - 1); diff --git a/zone/pathfinder_waypoint.h b/zone/pathfinder_waypoint.h index 8fc29d0c4..d5549c089 100644 --- a/zone/pathfinder_waypoint.h +++ b/zone/pathfinder_waypoint.h @@ -12,7 +12,7 @@ public: virtual ~PathfinderWaypoint(); virtual IPath FindRoute(const glm::vec3 &start, const glm::vec3 &end, bool &partial, bool &stuck, int flags = PathingNotDisabled); - virtual glm::vec3 GetRandomLocation(const glm::vec3 &start); + virtual glm::vec3 GetRandomLocation(const glm::vec3 &start, int flags = PathingNotDisabled); virtual void DebugCommand(Client *c, const Seperator *sep); private: diff --git a/zone/zone.cpp b/zone/zone.cpp index 0e08b665b..0280ee8f5 100644 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -2686,6 +2686,22 @@ uint32 Zone::GetSpawnKillCount(uint32 in_spawnid) { return 0; } +bool Zone::IsWaterZone(float z) +{ + + switch (GetZoneID()) { + case Zones::KEDGE: + return true; + case Zones::POWATER: + if (z < 0.0f) { + return true; + } + return false; + default: + return false; + } +} + void Zone::SetIsHotzone(bool is_hotzone) { Zone::is_hotzone = is_hotzone; diff --git a/zone/zone.h b/zone/zone.h index f1675cc22..c29a89b29 100755 --- a/zone/zone.h +++ b/zone/zone.h @@ -125,6 +125,7 @@ public: bool CanCastOutdoor() const { return (can_castoutdoor); } //qadar bool CanDoCombat() const { return (can_combat); } bool CanLevitate() const { return (can_levitate); } // Magoth78 + bool IsWaterZone(float z); bool Depop(bool StartSpawnTimer = false); bool did_adventure_actions; bool GetAuth(