2021-03-13 22:55:16 +00:00
|
|
|
#include "MobAI.hpp"
|
|
|
|
#include "Player.hpp"
|
2021-03-16 22:29:13 +00:00
|
|
|
#include "Racing.hpp"
|
|
|
|
#include "Transport.hpp"
|
|
|
|
#include "Nanos.hpp"
|
2021-03-13 22:55:16 +00:00
|
|
|
#include "Combat.hpp"
|
|
|
|
#include "Abilities.hpp"
|
2021-03-28 20:57:43 +00:00
|
|
|
#include "Rand.hpp"
|
2022-04-13 04:23:53 +00:00
|
|
|
#include "Items.hpp"
|
|
|
|
#include "Missions.hpp"
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
#include <limits.h>
|
|
|
|
|
2021-03-16 21:06:10 +00:00
|
|
|
using namespace MobAI;
|
|
|
|
|
2021-03-31 19:05:49 +00:00
|
|
|
bool MobAI::simulateMobs = settings::SIMULATEMOBS;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
/*
|
2021-03-16 22:29:13 +00:00
|
|
|
* Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and
|
2021-03-13 22:55:16 +00:00
|
|
|
* only returns the first step, since the rest will need to be recalculated anyway if chasing player.
|
|
|
|
*/
|
|
|
|
static std::pair<int,int> lerp(int x1, int y1, int x2, int y2, int speed) {
|
|
|
|
std::pair<int,int> ret = {x1, y1};
|
|
|
|
|
|
|
|
if (speed == 0)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
int distance = hypot(x1 - x2, y1 - y2);
|
|
|
|
|
|
|
|
if (distance > speed) {
|
|
|
|
|
|
|
|
int lerps = distance / speed;
|
|
|
|
|
|
|
|
// interpolate only the first point
|
|
|
|
float frac = 1.0f / lerps;
|
|
|
|
|
|
|
|
ret.first = (x1 + (x2 - x1) * frac);
|
|
|
|
ret.second = (y1 + (y2 - y1) * frac);
|
|
|
|
} else {
|
|
|
|
ret.first = x2;
|
|
|
|
ret.second = y2;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
void MobAI::clearDebuff(Mob *mob) {
|
|
|
|
mob->skillStyle = -1;
|
2021-06-20 18:37:37 +00:00
|
|
|
mob->cbf = 0;
|
2021-03-13 22:55:16 +00:00
|
|
|
mob->unbuffTimes.clear();
|
|
|
|
|
|
|
|
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
|
|
|
pkt1.eCT = 2;
|
2021-06-20 18:37:37 +00:00
|
|
|
pkt1.iID = mob->id;
|
|
|
|
pkt1.iConditionBitFlag = mob->cbf;
|
2021-03-13 22:55:16 +00:00
|
|
|
NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
|
|
|
}
|
|
|
|
|
|
|
|
void MobAI::followToCombat(Mob *mob) {
|
2022-04-13 19:56:12 +00:00
|
|
|
if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->kind == EntityKind::MOB) {
|
2021-04-07 00:43:43 +00:00
|
|
|
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
|
2021-03-13 22:55:16 +00:00
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
if (leadMob->groupMember[i] == 0)
|
|
|
|
break;
|
|
|
|
|
2022-04-13 19:56:12 +00:00
|
|
|
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
|
2021-03-13 22:55:16 +00:00
|
|
|
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
2021-04-07 00:43:43 +00:00
|
|
|
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 02:22:12 +00:00
|
|
|
if (followerMob->state != AIState::ROAMING) // only roaming mobs should transition to combat
|
2021-03-13 22:55:16 +00:00
|
|
|
continue;
|
|
|
|
|
2022-04-13 04:54:13 +00:00
|
|
|
followerMob->transition(AIState::COMBAT, mob->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 02:22:12 +00:00
|
|
|
if (leadMob->state != AIState::ROAMING)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
2022-04-13 04:54:13 +00:00
|
|
|
leadMob->transition(AIState::COMBAT, mob->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MobAI::groupRetreat(Mob *mob) {
|
2022-04-13 19:56:12 +00:00
|
|
|
if (NPCManager::NPCs.find(mob->groupLeader) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupLeader]->kind != EntityKind::MOB)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
2021-04-07 00:43:43 +00:00
|
|
|
Mob* leadMob = (Mob*)NPCManager::NPCs[mob->groupLeader];
|
2021-03-13 22:55:16 +00:00
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
if (leadMob->groupMember[i] == 0)
|
|
|
|
break;
|
|
|
|
|
2022-04-13 19:56:12 +00:00
|
|
|
if (NPCManager::NPCs.find(leadMob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[leadMob->groupMember[i]]->kind != EntityKind::MOB) {
|
2021-03-13 22:55:16 +00:00
|
|
|
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
2021-04-07 00:43:43 +00:00
|
|
|
Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]];
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 02:22:12 +00:00
|
|
|
if (followerMob->state != AIState::COMBAT)
|
2021-09-19 02:20:24 +00:00
|
|
|
continue;
|
|
|
|
|
2021-03-13 22:55:16 +00:00
|
|
|
followerMob->target = nullptr;
|
2022-04-13 02:22:12 +00:00
|
|
|
followerMob->state = AIState::RETREAT;
|
2021-03-13 22:55:16 +00:00
|
|
|
clearDebuff(followerMob);
|
|
|
|
}
|
|
|
|
|
2022-04-13 02:22:12 +00:00
|
|
|
if (leadMob->state != AIState::COMBAT)
|
2021-09-19 02:20:24 +00:00
|
|
|
return;
|
|
|
|
|
2021-03-13 22:55:16 +00:00
|
|
|
leadMob->target = nullptr;
|
2022-04-13 02:22:12 +00:00
|
|
|
leadMob->state = AIState::RETREAT;
|
2021-03-13 22:55:16 +00:00
|
|
|
clearDebuff(leadMob);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Aggro on nearby players.
|
|
|
|
* Even if they're in range, we can't assume they're all in the same one chunk
|
|
|
|
* as the mob, since it might be near a chunk boundary.
|
|
|
|
*/
|
|
|
|
bool MobAI::aggroCheck(Mob *mob, time_t currTime) {
|
|
|
|
CNSocket *closest = nullptr;
|
|
|
|
int closestDistance = INT_MAX;
|
|
|
|
|
2021-03-20 01:23:53 +00:00
|
|
|
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
|
2021-03-13 22:55:16 +00:00
|
|
|
Chunk* chunk = *it;
|
2021-03-21 02:54:24 +00:00
|
|
|
for (const EntityRef& ref : chunk->entities) {
|
|
|
|
// TODO: support targetting other CombatNPCs
|
2022-04-13 19:56:12 +00:00
|
|
|
if (ref.kind != EntityKind::PLAYER)
|
2021-03-21 02:54:24 +00:00
|
|
|
continue;
|
|
|
|
|
|
|
|
CNSocket *s = ref.sock;
|
2021-03-13 22:55:16 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(s);
|
|
|
|
|
2021-09-05 18:23:05 +00:00
|
|
|
if (plr->HP <= 0 || plr->onMonkey)
|
2021-03-13 22:55:16 +00:00
|
|
|
continue;
|
|
|
|
|
|
|
|
int mobRange = mob->sightRange;
|
|
|
|
|
|
|
|
if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH
|
2021-03-16 22:29:13 +00:00
|
|
|
|| Racing::EPRaces.find(s) != Racing::EPRaces.end())
|
2021-03-13 22:55:16 +00:00
|
|
|
mobRange /= 3;
|
|
|
|
|
|
|
|
// 0.33x - 1.66x the range
|
|
|
|
int levelDifference = plr->level - mob->level;
|
|
|
|
if (levelDifference > -10)
|
|
|
|
mobRange = levelDifference < 10 ? mobRange - (levelDifference * mobRange / 15) : mobRange / 3;
|
|
|
|
|
2022-04-13 02:22:12 +00:00
|
|
|
if (mob->state != AIState::ROAMING && plr->inCombat) // freshly out of aggro mobs
|
2021-03-13 22:55:16 +00:00
|
|
|
mobRange = mob->sightRange * 2; // should not be impacted by the above
|
|
|
|
|
|
|
|
if (plr->iSpecialState & (CN_SPECIAL_STATE_FLAG__INVISIBLE|CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
|
|
|
mobRange = -1;
|
|
|
|
|
|
|
|
// height is relevant for aggro distance because of platforming
|
2021-04-14 00:57:24 +00:00
|
|
|
int xyDistance = hypot(mob->x - plr->x, mob->y - plr->y);
|
|
|
|
int distance = hypot(xyDistance, (mob->z - plr->z) * 2); // difference in Z counts twice
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
if (distance > mobRange || distance > closestDistance)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// found a player
|
|
|
|
closest = s;
|
|
|
|
closestDistance = distance;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (closest != nullptr) {
|
|
|
|
// found closest player. engage.
|
2022-04-13 04:54:13 +00:00
|
|
|
mob->transition(AIState::COMBAT, closest);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
if (mob->groupLeader != 0)
|
|
|
|
followToCombat(mob);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void dealCorruption(Mob *mob, std::vector<int> targetData, int skillID, int style) {
|
|
|
|
Player *plr = PlayerManager::getPlayer(mob->target);
|
|
|
|
|
|
|
|
size_t resplen = sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT) + targetData[0] * sizeof(sCAttackResult);
|
|
|
|
|
|
|
|
// validate response packet
|
|
|
|
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT), targetData[0], sizeof(sCAttackResult))) {
|
|
|
|
std::cout << "[WARN] bad sP_FE2CL_NPC_SKILL_CORRUPTION_HIT packet size" << std::endl;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
|
|
|
memset(respbuf, 0, resplen);
|
|
|
|
|
|
|
|
sP_FE2CL_NPC_SKILL_CORRUPTION_HIT *resp = (sP_FE2CL_NPC_SKILL_CORRUPTION_HIT*)respbuf;
|
|
|
|
sCAttackResult *respdata = (sCAttackResult*)(respbuf+sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_HIT));
|
|
|
|
|
2021-06-20 18:37:37 +00:00
|
|
|
resp->iNPC_ID = mob->id;
|
2021-03-13 22:55:16 +00:00
|
|
|
resp->iSkillID = skillID;
|
|
|
|
resp->iStyle = style;
|
|
|
|
resp->iValue1 = plr->x;
|
|
|
|
resp->iValue2 = plr->y;
|
|
|
|
resp->iValue3 = plr->z;
|
|
|
|
resp->iTargetCnt = targetData[0];
|
|
|
|
|
|
|
|
for (int i = 0; i < targetData[0]; i++) {
|
|
|
|
CNSocket *sock = nullptr;
|
|
|
|
Player *plr = nullptr;
|
|
|
|
|
|
|
|
for (auto& pair : PlayerManager::players) {
|
|
|
|
if (pair.second->iID == targetData[i+1]) {
|
|
|
|
sock = pair.first;
|
|
|
|
plr = pair.second;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// player not found
|
|
|
|
if (plr == nullptr) {
|
|
|
|
std::cout << "[WARN] dealCorruption: player ID not found" << std::endl;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
respdata[i].eCT = 1;
|
|
|
|
respdata[i].iID = plr->iID;
|
|
|
|
respdata[i].bProtected = 0;
|
|
|
|
|
|
|
|
respdata[i].iActiveNanoSlotNum = -1;
|
|
|
|
for (int n = 0; n < 3; n++)
|
|
|
|
if (plr->activeNano == plr->equippedNanos[n])
|
|
|
|
respdata[i].iActiveNanoSlotNum = n;
|
|
|
|
respdata[i].iNanoID = plr->activeNano;
|
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
int style2 = Nanos::nanoStyle(plr->activeNano);
|
2021-03-13 22:55:16 +00:00
|
|
|
if (style2 == -1) { // no nano
|
2021-10-19 22:40:40 +00:00
|
|
|
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
|
2021-06-20 17:05:10 +00:00
|
|
|
respdata[i].iDamage = Abilities::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
2021-03-13 22:55:16 +00:00
|
|
|
} else if (style == style2) {
|
2021-10-19 22:40:40 +00:00
|
|
|
respdata[i].iHitFlag = HF_BIT_STYLE_TIE;
|
2021-03-13 22:55:16 +00:00
|
|
|
respdata[i].iDamage = 0;
|
|
|
|
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina;
|
|
|
|
} else if (style - style2 == 1 || style2 - style == 2) {
|
2021-10-19 22:40:40 +00:00
|
|
|
respdata[i].iHitFlag = HF_BIT_STYLE_WIN;
|
2021-03-13 22:55:16 +00:00
|
|
|
respdata[i].iDamage = 0;
|
|
|
|
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina += 45;
|
|
|
|
if (plr->Nanos[plr->activeNano].iStamina > 150)
|
|
|
|
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 150;
|
|
|
|
// fire damage power disguised as a corruption attack back at the enemy
|
2021-06-20 18:37:37 +00:00
|
|
|
std::vector<int> targetData2 = {1, mob->id, 0, 0, 0};
|
2021-06-20 17:05:10 +00:00
|
|
|
for (auto& pwr : Abilities::Powers)
|
2021-03-13 22:55:16 +00:00
|
|
|
if (pwr.skillType == EST_DAMAGE)
|
|
|
|
pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200);
|
|
|
|
} else {
|
2021-10-19 22:40:40 +00:00
|
|
|
respdata[i].iHitFlag = HF_BIT_STYLE_LOSE;
|
2021-06-20 17:05:10 +00:00
|
|
|
respdata[i].iDamage = Abilities::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500;
|
2021-03-13 22:55:16 +00:00
|
|
|
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina -= 90;
|
|
|
|
if (plr->Nanos[plr->activeNano].iStamina < 0) {
|
|
|
|
respdata[i].bNanoDeactive = 1;
|
|
|
|
respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE))
|
|
|
|
plr->HP -= respdata[i].iDamage;
|
|
|
|
|
|
|
|
respdata[i].iHP = plr->HP;
|
|
|
|
respdata[i].iConditionBitFlag = plr->iConditionBitFlag;
|
|
|
|
|
|
|
|
if (plr->HP <= 0) {
|
2022-04-13 03:29:11 +00:00
|
|
|
if (!MobAI::aggroCheck(mob, getTime()))
|
2022-04-13 03:44:12 +00:00
|
|
|
mob->transition(AIState::RETREAT, mob->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void useAbilities(Mob *mob, time_t currTime) {
|
|
|
|
/*
|
|
|
|
* targetData approach
|
|
|
|
* first integer is the count
|
|
|
|
* second to fifth integers are IDs, these can be either player iID or mob's iID
|
|
|
|
* whether the skill targets players or mobs is determined by the skill packet being fired
|
|
|
|
*/
|
|
|
|
Player *plr = PlayerManager::getPlayer(mob->target);
|
|
|
|
|
|
|
|
if (mob->skillStyle >= 0) { // corruption hit
|
|
|
|
int skillID = (int)mob->data["m_iCorruptionType"];
|
|
|
|
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
|
|
|
|
int temp = mob->skillStyle;
|
|
|
|
mob->skillStyle = -3; // corruption cooldown
|
|
|
|
mob->nextAttack = currTime + 1000;
|
|
|
|
dealCorruption(mob, targetData, skillID, temp);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mob->skillStyle == -2) { // eruption hit
|
|
|
|
int skillID = (int)mob->data["m_iMegaType"];
|
|
|
|
std::vector<int> targetData = {0, 0, 0, 0, 0};
|
|
|
|
|
|
|
|
// find the players within range of eruption
|
2021-03-20 01:23:53 +00:00
|
|
|
for (auto it = mob->viewableChunks.begin(); it != mob->viewableChunks.end(); it++) {
|
2021-03-13 22:55:16 +00:00
|
|
|
Chunk* chunk = *it;
|
2021-03-21 02:54:24 +00:00
|
|
|
for (const EntityRef& ref : chunk->entities) {
|
|
|
|
// TODO: see aggroCheck()
|
2022-04-13 19:56:12 +00:00
|
|
|
if (ref.kind != EntityKind::PLAYER)
|
2021-03-21 02:54:24 +00:00
|
|
|
continue;
|
|
|
|
|
|
|
|
CNSocket *s= ref.sock;
|
2021-03-13 22:55:16 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(s);
|
|
|
|
|
|
|
|
if (plr->HP <= 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y);
|
2021-06-20 17:05:10 +00:00
|
|
|
if (distance < Abilities::SkillTable[skillID].effectArea) {
|
2021-03-13 22:55:16 +00:00
|
|
|
targetData[0] += 1;
|
|
|
|
targetData[targetData[0]] = plr->iID;
|
|
|
|
if (targetData[0] > 3) // make sure not to have more than 4
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-20 17:05:10 +00:00
|
|
|
for (auto& pwr : Abilities::Powers)
|
|
|
|
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
|
2021-06-20 18:39:59 +00:00
|
|
|
pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
|
2021-03-13 22:55:16 +00:00
|
|
|
mob->skillStyle = -3; // eruption cooldown
|
|
|
|
mob->nextAttack = currTime + 1000;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mob->skillStyle == -3) { // cooldown expires
|
|
|
|
mob->skillStyle = -1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-28 20:57:43 +00:00
|
|
|
int random = Rand::rand(2000) * 1000;
|
2021-03-13 22:55:16 +00:00
|
|
|
int prob1 = (int)mob->data["m_iActiveSkill1Prob"]; // active skill probability
|
|
|
|
int prob2 = (int)mob->data["m_iCorruptionTypeProb"]; // corruption probability
|
|
|
|
int prob3 = (int)mob->data["m_iMegaTypeProb"]; // eruption probability
|
|
|
|
|
|
|
|
if (random < prob1) { // active skill hit
|
|
|
|
int skillID = (int)mob->data["m_iActiveSkill1"];
|
|
|
|
std::vector<int> targetData = {1, plr->iID, 0, 0, 0};
|
2021-06-20 17:05:10 +00:00
|
|
|
for (auto& pwr : Abilities::Powers)
|
|
|
|
if (pwr.skillType == Abilities::SkillTable[skillID].skillType) {
|
2021-03-13 22:55:16 +00:00
|
|
|
if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag))
|
|
|
|
return; // prevent debuffing a player twice
|
2021-06-20 18:39:59 +00:00
|
|
|
pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (random < prob1 + prob2) { // corruption windup
|
|
|
|
int skillID = (int)mob->data["m_iCorruptionType"];
|
|
|
|
INITSTRUCT(sP_FE2CL_NPC_SKILL_CORRUPTION_READY, pkt);
|
2021-06-20 18:37:37 +00:00
|
|
|
pkt.iNPC_ID = mob->id;
|
2021-03-13 22:55:16 +00:00
|
|
|
pkt.iSkillID = skillID;
|
|
|
|
pkt.iValue1 = plr->x;
|
|
|
|
pkt.iValue2 = plr->y;
|
|
|
|
pkt.iValue3 = plr->z;
|
2021-03-16 22:29:13 +00:00
|
|
|
mob->skillStyle = Nanos::nanoStyle(plr->activeNano) - 1;
|
2021-03-13 22:55:16 +00:00
|
|
|
if (mob->skillStyle == -1)
|
|
|
|
mob->skillStyle = 2;
|
|
|
|
if (mob->skillStyle == -2)
|
2021-03-28 20:57:43 +00:00
|
|
|
mob->skillStyle = Rand::rand(3);
|
2021-03-13 22:55:16 +00:00
|
|
|
pkt.iStyle = mob->skillStyle;
|
|
|
|
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_CORRUPTION_READY, sizeof(sP_FE2CL_NPC_SKILL_CORRUPTION_READY));
|
|
|
|
mob->nextAttack = currTime + 1800;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (random < prob1 + prob2 + prob3) { // eruption windup
|
|
|
|
int skillID = (int)mob->data["m_iMegaType"];
|
|
|
|
INITSTRUCT(sP_FE2CL_NPC_SKILL_READY, pkt);
|
2021-06-20 18:37:37 +00:00
|
|
|
pkt.iNPC_ID = mob->id;
|
2021-03-13 22:55:16 +00:00
|
|
|
pkt.iSkillID = skillID;
|
|
|
|
pkt.iValue1 = mob->hitX = plr->x;
|
|
|
|
pkt.iValue2 = mob->hitY = plr->y;
|
|
|
|
pkt.iValue3 = mob->hitZ = plr->z;
|
|
|
|
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_SKILL_READY, sizeof(sP_FE2CL_NPC_SKILL_READY));
|
|
|
|
mob->nextAttack = currTime + 1800;
|
|
|
|
mob->skillStyle = -2;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void drainMobHP(Mob *mob, int amount) {
|
|
|
|
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Damage);
|
|
|
|
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
|
|
|
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
|
|
|
|
|
|
|
memset(respbuf, 0, resplen);
|
|
|
|
|
|
|
|
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
|
|
|
sSkillResult_Damage *drain = (sSkillResult_Damage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
|
|
|
|
2021-06-20 18:37:37 +00:00
|
|
|
pkt->iID = mob->id;
|
2021-03-13 22:55:16 +00:00
|
|
|
pkt->eCT = 4; // mob
|
|
|
|
pkt->iTB_ID = ECSB_BOUNDINGBALL;
|
|
|
|
|
|
|
|
drain->eCT = 4;
|
2021-06-20 18:37:37 +00:00
|
|
|
drain->iID = mob->id;
|
2021-03-13 22:55:16 +00:00
|
|
|
drain->iDamage = amount;
|
2021-06-20 18:37:37 +00:00
|
|
|
drain->iHP = mob->hp -= amount;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
|
|
|
|
2021-06-20 18:37:37 +00:00
|
|
|
if (mob->hp <= 0)
|
2022-04-13 04:23:53 +00:00
|
|
|
mob->transition(AIState::DEAD, mob->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::incNextMovement(Mob* mob, time_t currTime) {
|
|
|
|
if (currTime == 0)
|
|
|
|
currTime = getTime();
|
|
|
|
|
|
|
|
int delay = (int)mob->data["m_iDelayTime"] * 1000;
|
|
|
|
mob->nextMovement = currTime + delay / 2 + Rand::rand(delay / 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Mob::step(time_t currTime) {
|
|
|
|
if (playersInView < 0)
|
|
|
|
std::cout << "[WARN] Weird playerview value " << playersInView << std::endl;
|
|
|
|
|
|
|
|
// skip movement and combat if disabled or not in view
|
|
|
|
if ((!MobAI::simulateMobs || playersInView == 0) && state != AIState::DEAD
|
|
|
|
&& state != AIState::RETREAT)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// call superclass step
|
|
|
|
CombatNPC::step(currTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MobAI::deadStep(CombatNPC* npc, time_t currTime) {
|
|
|
|
Mob* self = (Mob*)npc;
|
|
|
|
|
2021-03-13 22:55:16 +00:00
|
|
|
// despawn the mob after a short delay
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->killedTime != 0 && !self->despawned && currTime - self->killedTime > 2000) {
|
|
|
|
self->despawned = true;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
pkt.iNPC_ID = self->id;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// if it was summoned, mark it for removal
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->summoned) {
|
2021-03-13 22:55:16 +00:00
|
|
|
std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl;
|
2022-04-13 22:01:52 +00:00
|
|
|
NPCManager::queueNPCRemoval(self->id);
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// pre-set spawn coordinates if not marked for removal
|
2022-04-13 22:01:52 +00:00
|
|
|
self->x = self->spawnX;
|
|
|
|
self->y = self->spawnY;
|
|
|
|
self->z = self->spawnZ;
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// to guide their groupmates, group leaders still need to move despite being dead
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->groupLeader == self->id)
|
|
|
|
roamingStep(self, currTime);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->killedTime != 0 && currTime - self->killedTime < self->regenTime * 100)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
std::cout << "respawning mob " << self->id << " with HP = " << self->maxHealth << std::endl;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
self->transition(AIState::ROAMING, self->id);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// if mob is a group leader/follower, spawn where the group is.
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->groupLeader != 0) {
|
|
|
|
if (NPCManager::NPCs.find(self->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[self->groupLeader]->kind == EntityKind::MOB) {
|
|
|
|
Mob* leaderMob = (Mob*)NPCManager::NPCs[self->groupLeader];
|
|
|
|
self->x = leaderMob->x + self->offsetX;
|
|
|
|
self->y = leaderMob->y + self->offsetY;
|
|
|
|
self->z = leaderMob->z;
|
2021-03-13 22:55:16 +00:00
|
|
|
} else {
|
|
|
|
std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
pkt.NPCAppearanceData = self->getAppearanceData();
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// notify all nearby players
|
2022-04-13 22:01:52 +00:00
|
|
|
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
|
|
|
Mob* self = (Mob*)npc;
|
|
|
|
assert(self->target != nullptr);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// lose aggro if the player lost connection
|
2022-04-13 22:01:52 +00:00
|
|
|
if (PlayerManager::players.find(self->target) == PlayerManager::players.end()) {
|
|
|
|
if (!MobAI::aggroCheck(self, getTime()))
|
|
|
|
self->transition(AIState::RETREAT, self->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(self->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// lose aggro if the player became invulnerable or died
|
|
|
|
if (plr->HP <= 0
|
|
|
|
|| (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) {
|
2022-04-13 22:01:52 +00:00
|
|
|
if (!MobAI::aggroCheck(self, getTime()))
|
|
|
|
self->transition(AIState::RETREAT, self->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// drain
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->skillStyle < 0 && (self->lastDrainTime == 0 || currTime - self->lastDrainTime >= 1000)
|
|
|
|
&& self->cbf & CSB_BIT_BOUNDINGBALL) {
|
|
|
|
drainMobHP(self, self->maxHealth / 20); // lose 5% every second
|
|
|
|
self->lastDrainTime = currTime;
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if drain killed the mob, return early
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->hp <= 0)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
// unbuffing
|
2022-04-13 22:01:52 +00:00
|
|
|
std::unordered_map<int32_t, time_t>::iterator it = self->unbuffTimes.begin();
|
|
|
|
while (it != self->unbuffTimes.end()) {
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
if (currTime >= it->second) {
|
2022-04-13 22:01:52 +00:00
|
|
|
self->cbf &= ~it->first;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
|
|
|
|
pkt1.eCT = 2;
|
2022-04-13 22:01:52 +00:00
|
|
|
pkt1.iID = self->id;
|
|
|
|
pkt1.iConditionBitFlag = self->cbf;
|
|
|
|
NPCManager::sendToViewable(self, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
2021-03-28 20:57:43 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
it = self->unbuffTimes.erase(it);
|
2021-03-13 22:55:16 +00:00
|
|
|
} else {
|
|
|
|
it++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// skip attack if stunned or asleep
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
|
|
|
|
self->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
int distance = hypot(plr->x - self->x, plr->y - self->y);
|
|
|
|
int mobRange = (int)self->data["m_iAtkRange"] + (int)self->data["m_iRadius"];
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
if (currTime >= self->nextAttack) {
|
|
|
|
if (self->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
|
|
|
|
useAbilities(self, currTime);
|
|
|
|
if (self->target == nullptr)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int distanceToTravel = INT_MAX;
|
|
|
|
// movement logic: move when out of range but don't move while casting a skill
|
2022-04-13 22:01:52 +00:00
|
|
|
if (distance > mobRange && self->skillStyle == -1) {
|
|
|
|
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
2022-04-13 22:01:52 +00:00
|
|
|
self->nextMovement = currTime + 400;
|
|
|
|
if (currTime >= self->nextAttack)
|
|
|
|
self->nextAttack = 0;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// halve movement speed if snared
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
|
|
|
|
self->speed /= 2;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
int targetX = plr->x;
|
|
|
|
int targetY = plr->y;
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->groupLeader != 0) {
|
|
|
|
targetX += self->offsetX*distance/(self->idleRange + 1);
|
|
|
|
targetY += self->offsetY*distance/(self->idleRange + 1);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
distanceToTravel = std::min(distance-mobRange+1, self->speed*2/5);
|
|
|
|
auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel);
|
|
|
|
if (distanceToTravel < self->speed*2/5 && currTime >= self->nextAttack)
|
|
|
|
self->nextAttack = 0;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
pkt.iNPC_ID = self->id;
|
|
|
|
pkt.iSpeed = self->speed;
|
|
|
|
pkt.iToX = self->x = targ.first;
|
|
|
|
pkt.iToY = self->y = targ.second;
|
2021-03-13 22:55:16 +00:00
|
|
|
pkt.iToZ = plr->z;
|
|
|
|
pkt.iMoveStyle = 1;
|
|
|
|
|
|
|
|
// notify all nearby players
|
2022-04-13 22:01:52 +00:00
|
|
|
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
2021-03-28 20:57:43 +00:00
|
|
|
/* attack logic
|
2021-03-13 22:55:16 +00:00
|
|
|
* 2/5 represents 400 ms which is the time interval mobs use per movement logic step
|
|
|
|
* if the mob is one move interval away, we should just start attacking anyways.
|
|
|
|
*/
|
2022-04-13 22:01:52 +00:00
|
|
|
if (distance <= mobRange || distanceToTravel < self->speed*2/5) {
|
|
|
|
if (self->nextAttack == 0 || currTime >= self->nextAttack) {
|
|
|
|
self->nextAttack = currTime + (int)self->data["m_iDelayTime"] * 100;
|
|
|
|
Combat::npcAttackPc(self, currTime);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// retreat if the player leaves combat range
|
2022-04-13 22:01:52 +00:00
|
|
|
int xyDistance = hypot(plr->x - self->roamX, plr->y - self->roamY);
|
|
|
|
distance = hypot(xyDistance, plr->z - self->roamZ);
|
|
|
|
if (distance >= self->data["m_iCombatRange"]) {
|
|
|
|
self->transition(AIState::RETREAT, self->target);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
|
|
|
|
Mob* self = (Mob*)npc;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* We reuse nextAttack to avoid scanning for players all the time, but to still
|
|
|
|
* do so more often than if we waited for nextMovement (which is way too slow).
|
|
|
|
* In the case of group leaders, this step will be called by dead mobs, so disable attack.
|
|
|
|
*/
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->state != AIState::DEAD && (self->nextAttack == 0 || currTime >= self->nextAttack)) {
|
|
|
|
self->nextAttack = currTime + 500;
|
|
|
|
if (aggroCheck(self, currTime))
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// no random roaming if the mob already has a set path
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->staticPath)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->groupLeader != 0 && self->groupLeader != self->id) // don't roam by yourself without group leader
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* mob->nextMovement is also updated whenever the path queue is traversed in
|
2021-03-16 22:29:13 +00:00
|
|
|
* Transport::stepNPCPathing() (which ticks at a higher frequency than nextMovement),
|
2021-03-13 22:55:16 +00:00
|
|
|
* so we don't have to check if there's already entries in the queue since we know there won't be.
|
|
|
|
*/
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
2022-04-13 22:01:52 +00:00
|
|
|
incNextMovement(self, currTime);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
int xStart = self->spawnX - self->idleRange/2;
|
|
|
|
int yStart = self->spawnY - self->idleRange/2;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// some mobs don't move (and we mustn't divide/modulus by zero)
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->idleRange == 0 || self->speed == 0)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
int farX, farY, distance;
|
2022-04-13 22:01:52 +00:00
|
|
|
int minDistance = self->idleRange / 2;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// pick a random destination
|
2022-04-13 22:01:52 +00:00
|
|
|
farX = xStart + Rand::rand(self->idleRange);
|
|
|
|
farY = yStart + Rand::rand(self->idleRange);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
distance = std::abs(std::max(farX - self->x, farY - self->y));
|
2021-03-13 22:55:16 +00:00
|
|
|
if (distance == 0)
|
|
|
|
distance += 1; // hack to avoid FPE
|
|
|
|
|
|
|
|
// if it's too short a walk, go further in that direction
|
2022-04-13 22:01:52 +00:00
|
|
|
farX = self->x + (farX - self->x) * minDistance / distance;
|
|
|
|
farY = self->y + (farY - self->y) * minDistance / distance;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// but don't got out of bounds
|
2022-04-13 22:01:52 +00:00
|
|
|
farX = std::clamp(farX, xStart, xStart + self->idleRange);
|
|
|
|
farY = std::clamp(farY, yStart, yStart + self->idleRange);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// halve movement speed if snared
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
|
|
|
|
self->speed /= 2;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2021-05-01 15:04:28 +00:00
|
|
|
std::queue<Vec3> queue;
|
2022-04-13 22:01:52 +00:00
|
|
|
Vec3 from = { self->x, self->y, self->z };
|
|
|
|
Vec3 to = { farX, farY, self->z };
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2021-03-16 22:29:13 +00:00
|
|
|
// add a route to the queue; to be processed in Transport::stepNPCPathing()
|
2022-04-13 22:01:52 +00:00
|
|
|
Transport::lerp(&queue, from, to, self->speed);
|
|
|
|
Transport::NPCQueues[self->id] = queue;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->groupLeader != 0 && self->groupLeader == self->id) {
|
2021-03-13 22:55:16 +00:00
|
|
|
// make followers follow this npc.
|
|
|
|
for (int i = 0; i < 4; i++) {
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->groupMember[i] == 0)
|
2021-03-13 22:55:16 +00:00
|
|
|
break;
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
if (NPCManager::NPCs.find(self->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[self->groupMember[i]]->kind != EntityKind::MOB) {
|
2021-03-13 22:55:16 +00:00
|
|
|
std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-01 15:04:28 +00:00
|
|
|
std::queue<Vec3> queue2;
|
2022-04-13 22:01:52 +00:00
|
|
|
Mob* followerMob = (Mob*)NPCManager::NPCs[self->groupMember[i]];
|
2021-04-14 00:57:24 +00:00
|
|
|
from = { followerMob->x, followerMob->y, followerMob->z };
|
|
|
|
to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z };
|
2022-04-13 22:01:52 +00:00
|
|
|
Transport::lerp(&queue2, from, to, self->speed);
|
2021-06-20 18:37:37 +00:00
|
|
|
Transport::NPCQueues[followerMob->id] = queue2;
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::retreatStep(CombatNPC* npc, time_t currTime) {
|
|
|
|
Mob* self = (Mob*)npc;
|
|
|
|
|
|
|
|
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
2021-03-13 22:55:16 +00:00
|
|
|
return;
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
self->nextMovement = currTime + 400;
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
// distance between spawn point and current location
|
2022-04-13 22:01:52 +00:00
|
|
|
int distance = hypot(self->x - self->roamX, self->y - self->roamY);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
|
|
|
//if (distance > mob->data["m_iIdleRange"]) {
|
|
|
|
if (distance > 10) {
|
|
|
|
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
auto targ = lerp(self->x, self->y, self->roamX, self->roamY, (int)self->speed*4/5);
|
2021-03-13 22:55:16 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
pkt.iNPC_ID = self->id;
|
|
|
|
pkt.iSpeed = (int)self->speed * 2;
|
|
|
|
pkt.iToX = self->x = targ.first;
|
|
|
|
pkt.iToY = self->y = targ.second;
|
|
|
|
pkt.iToZ = self->z = self->spawnZ;
|
2021-03-13 22:55:16 +00:00
|
|
|
pkt.iMoveStyle = 1;
|
|
|
|
|
|
|
|
// notify all nearby players
|
2022-04-13 22:01:52 +00:00
|
|
|
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if we got there
|
|
|
|
//if (distance <= mob->data["m_iIdleRange"]) {
|
|
|
|
if (distance <= 10) { // retreat back to the spawn point
|
2022-04-13 22:01:52 +00:00
|
|
|
self->transition(AIState::ROAMING, self->id);
|
2021-03-13 22:55:16 +00:00
|
|
|
}
|
|
|
|
}
|
2022-04-13 03:29:11 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
|
|
|
|
Mob* self = (Mob*)npc;
|
2022-04-13 03:29:11 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
self->hp = self->maxHealth;
|
|
|
|
self->killedTime = 0;
|
|
|
|
self->nextAttack = 0;
|
|
|
|
self->cbf = 0;
|
2022-04-13 19:09:43 +00:00
|
|
|
|
|
|
|
// cast a return home heal spell, this is the right way(tm)
|
|
|
|
std::vector<int> targetData = { 1, 0, 0, 0, 0 };
|
|
|
|
for (auto& pwr : Abilities::Powers)
|
|
|
|
if (pwr.skillType == Abilities::SkillTable[110].skillType)
|
2022-04-13 22:01:52 +00:00
|
|
|
pwr.handle(self->id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]);
|
2022-04-13 19:09:43 +00:00
|
|
|
// clear outlying debuffs
|
2022-04-13 22:01:52 +00:00
|
|
|
clearDebuff(self);
|
2022-04-13 03:29:11 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
|
|
|
|
Mob* self = (Mob*)npc;
|
|
|
|
|
2022-04-13 19:56:12 +00:00
|
|
|
assert(src.kind == EntityKind::PLAYER);
|
2022-04-13 22:01:52 +00:00
|
|
|
self->target = src.sock;
|
|
|
|
self->nextMovement = getTime();
|
|
|
|
self->nextAttack = 0;
|
2022-04-13 04:54:13 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
self->roamX = self->x;
|
|
|
|
self->roamY = self->y;
|
|
|
|
self->roamZ = self->z;
|
2022-04-13 04:54:13 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
int skillID = (int)self->data["m_iPassiveBuff"]; // cast passive
|
|
|
|
std::vector<int> targetData = { 1, self->id, 0, 0, 0 };
|
2022-04-13 04:54:13 +00:00
|
|
|
for (auto& pwr : Abilities::Powers)
|
|
|
|
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
|
2022-04-13 22:01:52 +00:00
|
|
|
pwr.handle(self->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
|
2022-04-13 03:29:11 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
|
|
|
|
Mob* self = (Mob*)npc;
|
|
|
|
|
|
|
|
self->target = nullptr;
|
|
|
|
MobAI::clearDebuff(self);
|
|
|
|
if (self->groupLeader != 0)
|
|
|
|
MobAI::groupRetreat(self);
|
2022-04-13 03:29:11 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
|
|
|
|
Mob* self = (Mob*)npc;
|
|
|
|
|
|
|
|
self->target = nullptr;
|
|
|
|
self->cbf = 0;
|
|
|
|
self->skillStyle = -1;
|
|
|
|
self->unbuffTimes.clear();
|
|
|
|
self->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
2022-04-13 04:23:53 +00:00
|
|
|
|
|
|
|
// check for the edge case where hitting the mob did not aggro it
|
2022-04-13 19:56:12 +00:00
|
|
|
if (src.kind == EntityKind::PLAYER && src.isValid()) {
|
2022-04-13 04:23:53 +00:00
|
|
|
Player* plr = PlayerManager::getPlayer(src.sock);
|
|
|
|
|
|
|
|
Items::DropRoll rolled;
|
|
|
|
Items::DropRoll eventRolled;
|
|
|
|
std::map<int, int> qitemRolls;
|
|
|
|
|
|
|
|
Player* leader = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
|
|
|
assert(leader != nullptr); // should never happen
|
|
|
|
|
|
|
|
Combat::genQItemRolls(leader, qitemRolls);
|
|
|
|
|
|
|
|
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
|
2022-04-13 22:01:52 +00:00
|
|
|
Items::giveMobDrop(src.sock, self, rolled, eventRolled);
|
|
|
|
Missions::mobKilled(src.sock, self->type, qitemRolls);
|
2022-04-13 04:23:53 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
for (int i = 0; i < leader->groupCnt; i++) {
|
|
|
|
CNSocket* sockTo = PlayerManager::getSockFromID(leader->groupIDs[i]);
|
|
|
|
if (sockTo == nullptr)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
Player* otherPlr = PlayerManager::getPlayer(sockTo);
|
|
|
|
|
|
|
|
// only contribute to group members' kills if they're close enough
|
|
|
|
int dist = std::hypot(plr->x - otherPlr->x + 1, plr->y - otherPlr->y + 1);
|
|
|
|
if (dist > 5000)
|
|
|
|
continue;
|
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
Items::giveMobDrop(sockTo, self, rolled, eventRolled);
|
|
|
|
Missions::mobKilled(sockTo, self->type, qitemRolls);
|
2022-04-13 04:23:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// delay the despawn animation
|
2022-04-13 22:01:52 +00:00
|
|
|
self->despawned = false;
|
2022-04-13 04:23:53 +00:00
|
|
|
|
2022-04-13 22:01:52 +00:00
|
|
|
auto it = Transport::NPCQueues.find(self->id);
|
2022-04-13 04:23:53 +00:00
|
|
|
if (it == Transport::NPCQueues.end() || it->second.empty())
|
|
|
|
return;
|
|
|
|
|
|
|
|
// rewind or empty the movement queue
|
2022-04-13 22:01:52 +00:00
|
|
|
if (self->staticPath) {
|
2022-04-13 04:23:53 +00:00
|
|
|
/*
|
|
|
|
* This is inelegant, but we wind forward in the path until we find the point that
|
|
|
|
* corresponds with the Mob's spawn point.
|
|
|
|
*
|
|
|
|
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
|
|
|
|
*/
|
|
|
|
auto& queue = it->second;
|
2022-04-13 22:01:52 +00:00
|
|
|
for (auto point = queue.front(); point.x != self->spawnX || point.y != self->spawnY; point = queue.front()) {
|
2022-04-13 04:23:53 +00:00
|
|
|
queue.pop();
|
|
|
|
queue.push(point);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2022-04-13 22:01:52 +00:00
|
|
|
Transport::NPCQueues.erase(self->id);
|
2022-04-13 04:23:53 +00:00
|
|
|
}
|
2022-04-13 03:29:11 +00:00
|
|
|
}
|