mirror of
https://github.com/EQEmu/Server.git
synced 2026-04-06 12:12:25 +00:00
- License was intended to be GPLv3 per earlier commit of GPLv3 LICENSE FILE - This is confirmed by the inclusion of libraries that are incompatible with GPLv2 - This is also confirmed by KLS and the agreement of KLS's predecessors - Added GPLv3 license headers to the compilable source files - Removed Folly licensing in strings.h since the string functions do not match the Folly functions and are standard functions - this must have been left over from previous implementations - Removed individual contributor license headers since the project has been under the "developer" mantle for many years - Removed comments on files that were previously automatically generated since they've been manually modified multiple times and there are no automatic scripts referencing them (removed in 2023)
434 lines
12 KiB
C++
434 lines
12 KiB
C++
/* EQEmu: EQEmulator
|
|
|
|
Copyright (C) 2001-2026 EQEmu Development Team
|
|
|
|
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; either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "common/rulesys.h"
|
|
#include "zone/map.h"
|
|
#include "zone/water_map.h"
|
|
#include "zone/zone.h"
|
|
|
|
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 (IsPet() || IsCasting() || GetHP() == 0 || GetBodyType() == BodyType::Undead || (IsNPC() && CastToNPC()->IsUnderwaterOnly())) {
|
|
return;
|
|
}
|
|
|
|
//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;
|
|
}
|
|
|
|
//dont bother if we are immune to fleeing
|
|
if (GetSpecialAbility(SpecialAbility::FleeingImmunity) || spellbonuses.ImmuneToFlee) {
|
|
LogFlee("Mob [{}] is immune to fleeing via special ability or spell bonus", GetCleanName());
|
|
return;
|
|
}
|
|
|
|
int hp_ratio = GetIntHPRatio();
|
|
int flee_ratio = GetFleeRatio();
|
|
Mob *hate_top = GetHateTop();
|
|
|
|
// Sanity Check for race conditions
|
|
if(!hate_top) {
|
|
//this should never happen...
|
|
StartFleeing();
|
|
return;
|
|
}
|
|
|
|
bool mob_has_low_enough_health_to_flee = hp_ratio >= flee_ratio;
|
|
if (mob_has_low_enough_health_to_flee) {
|
|
LogFlee(
|
|
"Mob [{}] does not have low enough health to flee | hp_ratio [{}] flee_ratio [{}]",
|
|
GetCleanName(),
|
|
hp_ratio,
|
|
flee_ratio
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Sanity Check this should never happen...
|
|
if (!hate_top) {
|
|
currently_fleeing = true;
|
|
StartFleeing();
|
|
return;
|
|
}
|
|
|
|
int other_ratio = hate_top->GetIntHPRatio();
|
|
// If the Client is nearing death the NPC will not flee and instead try to kill the client.
|
|
if (other_ratio < 20) {
|
|
return;
|
|
}
|
|
|
|
// Flee Chance checking based on con.
|
|
uint32 con = GetLevelCon(hate_top->GetLevel(), GetLevel());
|
|
int flee_chance;
|
|
switch (con) {
|
|
//these values are not 100% researched
|
|
case ConsiderColor::Gray:
|
|
flee_chance = 100;
|
|
break;
|
|
case ConsiderColor::Green:
|
|
flee_chance = 90;
|
|
break;
|
|
case ConsiderColor::LightBlue:
|
|
flee_chance = 90;
|
|
break;
|
|
case ConsiderColor::DarkBlue:
|
|
flee_chance = 80;
|
|
break;
|
|
default:
|
|
flee_chance = 70;
|
|
break;
|
|
}
|
|
|
|
LogFlee(
|
|
"Post con-switch | Mob [{}] con [{}] hp_ratio [{}] flee_ratio [{}] flee_chance [{}]",
|
|
GetCleanName(),
|
|
con,
|
|
hp_ratio,
|
|
flee_ratio,
|
|
flee_chance
|
|
);
|
|
|
|
// If we got here we are allowed to roll on flee chance if there is not other hated NPC's in the area.
|
|
// SpecialAbility::AlwaysFlee, skip roll
|
|
// if FleeIfNotAlone is true, we skip alone check
|
|
// roll chance
|
|
if (GetSpecialAbility(SpecialAbility::AlwaysFlee) ||
|
|
((RuleB(Combat, FleeIfNotAlone) || entity_list.FleeAllyCount(hate_top, this) == 0) &&
|
|
zone->random.Roll(flee_chance))) {
|
|
|
|
LogFlee(
|
|
"Passed all checks to flee | Mob [{}] con [{}] hp_ratio [{}] flee_ratio [{}] flee_chance [{}]",
|
|
GetCleanName(),
|
|
con,
|
|
hp_ratio,
|
|
flee_ratio,
|
|
flee_chance
|
|
);
|
|
|
|
currently_fleeing = true;
|
|
StartFleeing();
|
|
}
|
|
}
|
|
|
|
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.
|
|
if (flee_mode && (GetSpecialAbility(SpecialAbility::FleeingImmunity) || spellbonuses.ImmuneToFlee) &&
|
|
!spellbonuses.IsFeared && !spellbonuses.IsBlind) {
|
|
currently_fleeing = false;
|
|
return;
|
|
}
|
|
|
|
Mob *hate_top = GetHateTop();
|
|
bool dying = GetIntHPRatio() < GetFleeRatio();
|
|
|
|
// We have stopped fleeing for an unknown reason (couldn't find a node is possible) restart.
|
|
if (flee_mode && !currently_fleeing) {
|
|
if(dying) {
|
|
StartFleeing();
|
|
}
|
|
}
|
|
|
|
//see if we are still dying, if so, do nothing
|
|
if (dying) {
|
|
return;
|
|
}
|
|
|
|
//we are not dying anymore, check to make sure we're not blind or feared and cancel flee.
|
|
StopFleeing();
|
|
}
|
|
|
|
void Mob::CalculateNewFearpoint()
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
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(SpellEffect::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;
|
|
}
|
|
|