From 2f3f8a39510fd37f67cb51958cbb74a4559a053e Mon Sep 17 00:00:00 2001 From: gsemaj Date: Mon, 11 Apr 2022 10:26:57 -0400 Subject: [PATCH] [refactor] Initial ICombatant draft --- src/Abilities.cpp | 39 ++-- src/Chunking.cpp | 8 +- src/Combat.cpp | 345 ++++++++++++++++---------------- src/Combat.hpp | 5 +- src/CustomCommands.cpp | 2 +- src/Entities.hpp | 64 ++++-- src/MobAI.cpp | 432 ++++++++++++++++++++++------------------- src/MobAI.hpp | 34 ++-- src/NPCManager.cpp | 2 +- src/Player.hpp | 10 +- src/Transport.cpp | 2 +- src/core/Packets.hpp | 2 +- 12 files changed, 505 insertions(+), 440 deletions(-) diff --git a/src/Abilities.cpp b/src/Abilities.cpp index cc613a4..d0c2a44 100644 --- a/src/Abilities.cpp +++ b/src/Abilities.cpp @@ -148,12 +148,12 @@ bool doDebuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t target } Mob* mob = (Mob*)npc; - Combat::hitMob(sock, mob, 0); + mob->takeDamage(sock, 0); respdata[i].eCT = 4; respdata[i].iID = mob->id; respdata[i].bProtected = 1; - if (mob->skillStyle < 0 && mob->state != MobState::RETREAT + if (mob->skillStyle < 0 && mob->state != AIState::RETREAT && !(mob->cbf & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom mob->cbf |= bitFlag; mob->unbuffTimes[bitFlag] = getTime() + duration * 100; @@ -220,14 +220,14 @@ bool doDamageNDebuff(CNSocket *sock, sSkillResult_Damage_N_Debuff *respdata, int Mob* mob = (Mob*)npc; - Combat::hitMob(sock, mob, 0); // just to gain aggro + mob->takeDamage(sock, 0); respdata[i].eCT = 4; respdata[i].iDamage = duration / 10; respdata[i].iID = mob->id; respdata[i].iHP = mob->hp; respdata[i].bProtected = 1; - if (mob->skillStyle < 0 && mob->state != MobState::RETREAT + if (mob->skillStyle < 0 && mob->state != AIState::RETREAT && !(mob->cbf & CSB_BIT_FREEDOM)) { // only debuff if the enemy is not retreating, casting corruption or in freedom mob->cbf |= bitFlag; mob->unbuffTimes[bitFlag] = getTime() + duration * 100; @@ -289,7 +289,7 @@ bool doDamage(CNSocket *sock, sSkillResult_Damage *respdata, int i, int32_t targ Mob* mob = (Mob*)npc; Player *plr = PlayerManager::getPlayer(sock); - int damage = Combat::hitMob(sock, mob, std::max(PC_MAXHEALTH(plr->level) * amount / 1000, mob->maxHealth * amount / 1000)); + int damage = mob->takeDamage(sock, std::max(PC_MAXHEALTH(plr->level) * amount / 1000, mob->maxHealth * amount / 1000)); respdata[i].eCT = 4; respdata[i].iDamage = damage; @@ -344,7 +344,7 @@ bool doLeech(CNSocket *sock, sSkillResult_Heal_HP *healdata, int i, int32_t targ Mob* mob = (Mob*)npc; - int damage = Combat::hitMob(sock, mob, amount * 2); + int damage = mob->takeDamage(sock, amount * 2); damagedata->eCT = 4; damagedata->iDamage = damage; @@ -449,13 +449,8 @@ bool doDamageNDebuff(Mob* mob, sSkillResult_Damage_N_Debuff* respdata, int i, in respdata[i].iConditionBitFlag = plr->iConditionBitFlag; if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, getTime())) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } + if (!MobAI::aggroCheck(mob, getTime())) + mob->transition(AIState::RETREAT, mob->target); } return true; @@ -529,13 +524,8 @@ bool doDamage(Mob* mob, sSkillResult_Damage* respdata, int i, int32_t targetID, respdata[i].iHP = plr->HP -= damage; if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, getTime())) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } + if (!MobAI::aggroCheck(mob, getTime())) + mob->transition(AIState::RETREAT, mob->target); } return true; @@ -587,13 +577,8 @@ bool doLeech(Mob* mob, sSkillResult_Heal_HP* healdata, int i, int32_t targetID, damagedata->iHP = plr->HP -= damage; if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, getTime())) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } + if (!MobAI::aggroCheck(mob, getTime())) + mob->transition(AIState::RETREAT, mob->target); } return true; diff --git a/src/Chunking.cpp b/src/Chunking.cpp index cbb439b..d26fe8d 100644 --- a/src/Chunking.cpp +++ b/src/Chunking.cpp @@ -77,7 +77,7 @@ void Chunking::untrackEntity(ChunkPos chunkPos, const EntityRef& ref) { void Chunking::addEntityToChunks(std::set chnks, const EntityRef& ref) { Entity *ent = ref.getEntity(); - bool alive = ent->isAlive(); + bool alive = ent->isExtant(); // TODO: maybe optimize this, potentially using AROUND packets? for (Chunk *chunk : chnks) { @@ -94,7 +94,7 @@ void Chunking::addEntityToChunks(std::set chnks, const EntityRef& ref) { } // notify this *player* of the existence of all visible Entities - if (ref.type == EntityType::PLAYER && other->isAlive()) { + if (ref.type == EntityType::PLAYER && other->isExtant()) { other->enterIntoViewOf(ref.sock); } @@ -109,7 +109,7 @@ void Chunking::addEntityToChunks(std::set chnks, const EntityRef& ref) { void Chunking::removeEntityFromChunks(std::set chnks, const EntityRef& ref) { Entity *ent = ref.getEntity(); - bool alive = ent->isAlive(); + bool alive = ent->isExtant(); // TODO: same as above for (Chunk *chunk : chnks) { @@ -126,7 +126,7 @@ void Chunking::removeEntityFromChunks(std::set chnks, const EntityRef& r } // notify this *player* of the departure of all visible Entities - if (ref.type == EntityType::PLAYER && other->isAlive()) { + if (ref.type == EntityType::PLAYER && other->isExtant()) { other->disappearFromViewOf(ref.sock); } diff --git a/src/Combat.cpp b/src/Combat.cpp index 2e7f76c..1b83cdf 100644 --- a/src/Combat.cpp +++ b/src/Combat.cpp @@ -17,6 +17,148 @@ using namespace Combat; /// Player Id -> Bullet Id -> Bullet std::map> Combat::Bullets; +int Player::takeDamage(EntityRef src, int amt) { + HP -= amt; + return amt; +} + +void Player::heal(EntityRef src, int amt) { + // stubbed +} + +bool Player::isAlive() { + return HP > 0; +} + +int Player::getCurrentHP() { + return HP; +} + +int32_t Player::getID() { + return iID; +} + +void Player::step(time_t currTime) { + // no-op +} + +int CombatNPC::takeDamage(EntityRef src, int amt) { + + /* REFACTOR: all of this logic is strongly coupled to mobs. + * come back to this when more of it is moved to CombatNPC. + * remove this cast when done */ + Mob* mob = (Mob*)this; + + // cannot kill mobs multiple times; cannot harm retreating mobs + if (mob->state != AIState::ROAMING && mob->state != AIState::COMBAT) { + return 0; // no damage + } + + if (mob->skillStyle >= 0) + return 0; // don't hurt a mob casting corruption + + if (mob->state == AIState::ROAMING) { + assert(mob->target == nullptr && src.type == EntityType::PLAYER); // players only for now + mob->transition(AIState::COMBAT, src); + + if (mob->groupLeader != 0) + MobAI::followToCombat(mob); + } + + hp -= amt; + + // wake up sleeping monster + if (mob->cbf & CSB_BIT_MEZ) { + mob->cbf &= ~CSB_BIT_MEZ; + + INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); + pkt1.eCT = 2; + pkt1.iID = mob->id; + pkt1.iConditionBitFlag = mob->cbf; + NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); + } + + if (mob->hp <= 0) + transition(AIState::DEAD, src); + + return amt; +} + +void CombatNPC::heal(EntityRef src, int amt) { + // stubbed +} + +bool CombatNPC::isAlive() { + return hp > 0; +} + +int CombatNPC::getCurrentHP() { + return hp; +} + +int32_t CombatNPC::getID() { + return id; +} + +void CombatNPC::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; + + switch (state) { + case AIState::INACTIVE: + // no-op + break; + case AIState::ROAMING: + roamingStep(currTime); + break; + case AIState::COMBAT: + combatStep(currTime); + break; + case AIState::RETREAT: + retreatStep(currTime); + break; + case AIState::DEAD: + deadStep(currTime); + break; + } +} + +void CombatNPC::transition(AIState newState, EntityRef src) { + state = newState; + switch (newState) { + case AIState::INACTIVE: + onInactive(); + break; + case AIState::ROAMING: + onRoamStart(); + break; + case AIState::COMBAT: + /* TODO: fire any triggered events + for (NPCEvent& event : NPCManager::NPCEvents) + if (event.trigger == ON_COMBAT && event.npcType == type) + event.handler(src, this); + */ + onCombatStart(src); + break; + case AIState::RETREAT: + onRetreat(); + break; + case AIState::DEAD: + /* TODO: fire any triggered events + for (NPCEvent& event : NPCManager::NPCEvents) + if (event.trigger == ON_KILLED && event.npcType == type) + event.handler(src, this); + */ + onDeath(src); + break; + } +} + static std::pair getDamage(int attackPower, int defensePower, bool shouldCrit, bool batteryBoost, int attackerStyle, int defenderStyle, int difficulty) { @@ -137,7 +279,7 @@ static void pcAttackNpcs(CNSocket *sock, CNPacketData *data) { else plr->batteryW = 0; - damage.first = hitMob(sock, mob, damage.first); + damage.first = mob->takeDamage(sock, damage.first); respdata[i].iID = mob->id; respdata[i].iDamage = damage.first; @@ -180,52 +322,11 @@ void Combat::npcAttackPc(Mob *mob, time_t currTime) { PlayerManager::sendToViewable(mob->target, respbuf, P_FE2CL_NPC_ATTACK_PCs); if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!MobAI::aggroCheck(mob, currTime)) { - MobAI::clearDebuff(mob); - if (mob->groupLeader != 0) - MobAI::groupRetreat(mob); - } + if (!MobAI::aggroCheck(mob, getTime())) + mob->transition(AIState::RETREAT, mob->target); } } -int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) { - // cannot kill mobs multiple times; cannot harm retreating mobs - if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) { - return 0; // no damage - } - - if (mob->skillStyle >= 0) - return 0; // don't hurt a mob casting corruption - - if (mob->state == MobState::ROAMING) { - assert(mob->target == nullptr); - MobAI::enterCombat(sock, mob); - - if (mob->groupLeader != 0) - MobAI::followToCombat(mob); - } - - mob->hp -= damage; - - // wake up sleeping monster - if (mob->cbf & CSB_BIT_MEZ) { - mob->cbf &= ~CSB_BIT_MEZ; - - INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); - pkt1.eCT = 2; - pkt1.iID = mob->id; - pkt1.iConditionBitFlag = mob->cbf; - NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); - } - - if (mob->hp <= 0) - killMob(mob->target, mob); - - return damage; -} - /* * When a group of players is doing missions together, we want them to all get * quest items at the same time, but we don't want the odds of quest item @@ -233,7 +334,7 @@ int Combat::hitMob(CNSocket *sock, Mob *mob, int damage) { * single RNG roll per mission task, and every group member shares that same * set of rolls. */ -static void genQItemRolls(Player *leader, std::map& rolls) { +void Combat::genQItemRolls(Player *leader, std::map& rolls) { for (int i = 0; i < leader->groupCnt; i++) { if (leader->groupIDs[i] == 0) continue; @@ -250,79 +351,6 @@ static void genQItemRolls(Player *leader, std::map& rolls) { } } -void Combat::killMob(CNSocket *sock, Mob *mob) { - mob->state = MobState::DEAD; - mob->target = nullptr; - mob->cbf = 0; - mob->skillStyle = -1; - mob->unbuffTimes.clear(); - mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step? - - // check for the edge case where hitting the mob did not aggro it - if (sock != nullptr) { - Player* plr = PlayerManager::getPlayer(sock); - - Items::DropRoll rolled; - Items::DropRoll eventRolled; - std::map qitemRolls; - - Player *leader = PlayerManager::getPlayerFromID(plr->iIDGroup); - assert(leader != nullptr); // should never happen - - genQItemRolls(leader, qitemRolls); - - if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) { - Items::giveMobDrop(sock, mob, rolled, eventRolled); - Missions::mobKilled(sock, mob->type, qitemRolls); - } 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; - - Items::giveMobDrop(sockTo, mob, rolled, eventRolled); - Missions::mobKilled(sockTo, mob->type, qitemRolls); - } - } - } - - // delay the despawn animation - mob->despawned = false; - - // fire any triggered events - for (NPCEvent& event : NPCManager::NPCEvents) - if (event.trigger == ON_KILLED && event.npcType == mob->type) - event.handler(sock, mob); - - auto it = Transport::NPCQueues.find(mob->id); - if (it == Transport::NPCQueues.end() || it->second.empty()) - return; - - // rewind or empty the movement queue - if (mob->staticPath) { - /* - * 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; - for (auto point = queue.front(); point.x != mob->spawnX || point.y != mob->spawnY; point = queue.front()) { - queue.pop(); - queue.push(point); - } - } else { - Transport::NPCQueues.erase(mob->id); - } -} - static void combatBegin(CNSocket *sock, CNPacketData *data) { Player *plr = PlayerManager::getPlayer(sock); @@ -417,7 +445,7 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) { return; // Unlike the attack mob packet, attacking players packet has an 8-byte trail (Instead of 4 bytes). - if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(int32_t) * 2, data->size)) { + if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs), pkt->iTargetCnt, sizeof(sGM_PVPTarget), data->size)) { std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n"; return; } @@ -441,11 +469,20 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) { resp->iTargetCnt = pkt->iTargetCnt; for (int i = 0; i < pkt->iTargetCnt; i++) { - if (pktdata[i*2+1] == 1) { // eCT == 1; attack player - Player *target = nullptr; + + ICombatant* target = nullptr; + sGM_PVPTarget* targdata = (sGM_PVPTarget*)(pktdata + i * 2); + std::pair damage; + + if (pkt->iTargetCnt > 1) + damage.first = plr->groupDamage; + else + damage.first = plr->pointDamage; + + if (targdata->eCT == 1) { // eCT == 1; attack player for (auto& pair : PlayerManager::players) { - if (pair.second->iID == pktdata[i*2]) { + if (pair.second->iID == targdata->iID) { target = pair.second; break; } @@ -457,67 +494,41 @@ static void pcAttackChars(CNSocket *sock, CNPacketData *data) { return; } - std::pair damage; + damage = getDamage(damage.first, ((Player*)target)->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0); - if (pkt->iTargetCnt > 1) - damage.first = plr->groupDamage; - else - damage.first = plr->pointDamage; - - damage = getDamage(damage.first, target->defense, true, (plr->batteryW > 6 + plr->level), -1, -1, 0); - - if (plr->batteryW >= 6 + plr->level) - plr->batteryW -= 6 + plr->level; - else - plr->batteryW = 0; - - target->HP -= damage.first; - - respdata[i].eCT = pktdata[i*2+1]; - respdata[i].iID = target->iID; - respdata[i].iDamage = damage.first; - respdata[i].iHP = target->HP; - respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade } else { // eCT == 4; attack mob - if (NPCManager::NPCs.find(pktdata[i*2]) == NPCManager::NPCs.end()) { + + if (NPCManager::NPCs.find(targdata->iID) == NPCManager::NPCs.end()) { // not sure how to best handle this std::cout << "[WARN] pcAttackChars: NPC ID not found" << std::endl; return; } - BaseNPC* npc = NPCManager::NPCs[pktdata[i * 2]]; + BaseNPC* npc = NPCManager::NPCs[targdata->iID]; if (npc->kind != EntityType::MOB) { std::cout << "[WARN] pcAttackChars: NPC is not a mob" << std::endl; return; } Mob* mob = (Mob*)npc; - - std::pair damage; - - if (pkt->iTargetCnt > 1) - damage.first = plr->groupDamage; - else - damage.first = plr->pointDamage; - + target = mob; int difficulty = (int)mob->data["m_iNpcLevel"]; - damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty), Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); - - if (plr->batteryW >= 6 + difficulty) - plr->batteryW -= 6 + difficulty; - else - plr->batteryW = 0; - - damage.first = hitMob(sock, mob, damage.first); - - respdata[i].eCT = pktdata[i*2+1]; - respdata[i].iID = mob->id; - respdata[i].iDamage = damage.first; - respdata[i].iHP = mob->hp; - respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade } + + if (plr->batteryW >= 6 + plr->level) + plr->batteryW -= 6 + plr->level; + else + plr->batteryW = 0; + + damage.first = target->takeDamage(sock, damage.first); + + respdata[i].eCT = targdata->eCT; + respdata[i].iID = target->getID(); + respdata[i].iDamage = damage.first; + respdata[i].iHP = target->getCurrentHP(); + respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade } sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen); @@ -709,7 +720,7 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) { int difficulty = (int)mob->data["m_iNpcLevel"]; damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); - damage.first = hitMob(sock, mob, damage.first); + damage.first = mob->takeDamage(sock, damage.first); respdata[i].iID = mob->id; respdata[i].iDamage = damage.first; diff --git a/src/Combat.hpp b/src/Combat.hpp index 89863d1..14d0076 100644 --- a/src/Combat.hpp +++ b/src/Combat.hpp @@ -17,12 +17,13 @@ struct Bullet { int bulletType; }; + + namespace Combat { extern std::map> Bullets; void init(); void npcAttackPc(Mob *mob, time_t currTime); - int hitMob(CNSocket *sock, Mob *mob, int damage); - void killMob(CNSocket *sock, Mob *mob); + void genQItemRolls(Player* leader, std::map& rolls); } diff --git a/src/CustomCommands.cpp b/src/CustomCommands.cpp index 2d92c21..4c2bb9e 100644 --- a/src/CustomCommands.cpp +++ b/src/CustomCommands.cpp @@ -328,7 +328,7 @@ static void toggleAiCommand(std::string full, std::vector& args, CN continue; Mob* mob = (Mob*)pair.second; - mob->state = MobState::RETREAT; + mob->state = AIState::RETREAT; mob->target = nullptr; mob->nextMovement = getTime(); diff --git a/src/Entities.hpp b/src/Entities.hpp index 3e613ce..638b7d8 100644 --- a/src/Entities.hpp +++ b/src/Entities.hpp @@ -16,6 +16,16 @@ enum class EntityType : uint8_t { BUS }; +enum class AIState { + INACTIVE, + ROAMING, + COMBAT, + RETREAT, + DEAD +}; + +class Chunk; + struct Entity { EntityType kind = EntityType::INVALID; int x = 0, y = 0, z = 0; @@ -26,7 +36,7 @@ struct Entity { // destructor must be virtual, apparently virtual ~Entity() {} - virtual bool isAlive() { return true; } + virtual bool isExtant() { return true; } // stubs virtual void enterIntoViewOf(CNSocket *sock) = 0; @@ -69,6 +79,24 @@ struct EntityRef { } }; +/* + * Interfaces + */ + +class ICombatant { +public: + ICombatant() {} + virtual ~ICombatant() {} + + virtual int takeDamage(EntityRef, int) = 0; + virtual void heal(EntityRef, int) = 0; + virtual bool isAlive() = 0; + virtual int getCurrentHP() = 0; + virtual int32_t getID() = 0; + + virtual void step(time_t currTime) = 0; +}; + /* * Subclasses */ @@ -98,15 +126,15 @@ public: sNPCAppearanceData getAppearanceData(); }; -struct CombatNPC : public BaseNPC { +struct CombatNPC : public BaseNPC, public ICombatant { int maxHealth = 0; int spawnX = 0; int spawnY = 0; int spawnZ = 0; int level = 0; int speed = 300; - - void (*_stepAI)(CombatNPC*, time_t) = nullptr; + AIState state = AIState::INACTIVE; + int playersInView = 0; // for optimizing away AI in empty chunks CombatNPC(int x, int y, int z, int angle, uint64_t iID, int t, int id, int maxHP) : BaseNPC(angle, iID, t, id), maxHealth(maxHP) { @@ -115,17 +143,30 @@ struct CombatNPC : public BaseNPC { spawnZ = z; } - virtual void stepAI(time_t currTime) { - if (_stepAI != nullptr) - _stepAI(this, currTime); - } + virtual bool isExtant() override { return hp > 0; } - virtual bool isAlive() override { return hp > 0; } + virtual int takeDamage(EntityRef src, int amt) override; + virtual void heal(EntityRef src, int amt) override; + virtual bool isAlive() override; + virtual int getCurrentHP() override; + virtual int32_t getID() override; + + virtual void step(time_t currTime) override; + virtual void roamingStep(time_t currTime) {} // no-ops by default + virtual void combatStep(time_t currTime) {} + virtual void retreatStep(time_t currTime) {} + virtual void deadStep(time_t currTime) {} + + virtual void transition(AIState newState, EntityRef src); + virtual void onInactive() {} // no-ops by default + virtual void onRoamStart() {} + virtual void onCombatStart(EntityRef src) {} + virtual void onRetreat() {} + virtual void onDeath(EntityRef src) {} }; // Mob is in MobAI.hpp, Player is in Player.hpp -// TODO: decouple from BaseNPC struct Egg : public BaseNPC { bool summoned = false; bool dead = false; @@ -137,13 +178,12 @@ struct Egg : public BaseNPC { kind = EntityType::EGG; } - virtual bool isAlive() override { return !dead; } + virtual bool isExtant() override { return !dead; } virtual void enterIntoViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override; }; -// TODO: decouple from BaseNPC struct Bus : public BaseNPC { Bus(int angle, uint64_t iID, int t, int id) : BaseNPC(angle, iID, t, id) { diff --git a/src/MobAI.cpp b/src/MobAI.cpp index 3f4c020..cc73de5 100644 --- a/src/MobAI.cpp +++ b/src/MobAI.cpp @@ -6,6 +6,8 @@ #include "Combat.hpp" #include "Abilities.hpp" #include "Rand.hpp" +#include "Items.hpp" +#include "Missions.hpp" #include #include @@ -14,8 +16,6 @@ using namespace MobAI; bool MobAI::simulateMobs = settings::SIMULATEMOBS; -static void roamingStep(Mob *mob, time_t currTime); - /* * Dynamic lerp; distinct from Transport::lerp(). This one doesn't care about height and * only returns the first step, since the rest will need to be recalculated anyway if chasing player. @@ -70,16 +70,16 @@ void MobAI::followToCombat(Mob *mob) { } Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]]; - if (followerMob->state != MobState::ROAMING) // only roaming mobs should transition to combat + if (followerMob->state != AIState::ROAMING) // only roaming mobs should transition to combat continue; - enterCombat(mob->target, followerMob); + followerMob->transition(AIState::COMBAT, mob->target); } - if (leadMob->state != MobState::ROAMING) + if (leadMob->state != AIState::ROAMING) return; - enterCombat(mob->target, leadMob); + leadMob->transition(AIState::COMBAT, mob->target); } } @@ -98,19 +98,19 @@ void MobAI::groupRetreat(Mob *mob) { } Mob* followerMob = (Mob*)NPCManager::NPCs[leadMob->groupMember[i]]; - if (followerMob->state != MobState::COMBAT) + if (followerMob->state != AIState::COMBAT) continue; followerMob->target = nullptr; - followerMob->state = MobState::RETREAT; + followerMob->state = AIState::RETREAT; clearDebuff(followerMob); } - if (leadMob->state != MobState::COMBAT) + if (leadMob->state != AIState::COMBAT) return; leadMob->target = nullptr; - leadMob->state = MobState::RETREAT; + leadMob->state = AIState::RETREAT; clearDebuff(leadMob); } @@ -147,7 +147,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) { if (levelDifference > -10) mobRange = levelDifference < 10 ? mobRange - (levelDifference * mobRange / 15) : mobRange / 3; - if (mob->state != MobState::ROAMING && plr->inCombat) // freshly out of aggro mobs + if (mob->state != AIState::ROAMING && plr->inCombat) // freshly out of aggro mobs mobRange = mob->sightRange * 2; // should not be impacted by the above if (plr->iSpecialState & (CN_SPECIAL_STATE_FLAG__INVISIBLE|CN_SPECIAL_STATE_FLAG__INVULNERABLE)) @@ -168,7 +168,7 @@ bool MobAI::aggroCheck(Mob *mob, time_t currTime) { if (closest != nullptr) { // found closest player. engage. - enterCombat(closest, mob); + mob->transition(AIState::COMBAT, closest); if (mob->groupLeader != 0) followToCombat(mob); @@ -268,13 +268,8 @@ static void dealCorruption(Mob *mob, std::vector targetData, int skillID, i respdata[i].iConditionBitFlag = plr->iConditionBitFlag; if (plr->HP <= 0) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!aggroCheck(mob, getTime())) { - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); - } + if (!MobAI::aggroCheck(mob, getTime())) + mob->transition(AIState::RETREAT, mob->target); } } @@ -395,27 +390,6 @@ static void useAbilities(Mob *mob, time_t currTime) { return; } -void MobAI::enterCombat(CNSocket *sock, Mob *mob) { - mob->target = sock; - mob->state = MobState::COMBAT; - mob->nextMovement = getTime(); - mob->nextAttack = 0; - - mob->roamX = mob->x; - mob->roamY = mob->y; - mob->roamZ = mob->z; - - int skillID = (int)mob->data["m_iPassiveBuff"]; // cast passive - std::vector targetData = {1, mob->id, 0, 0, 0}; - for (auto& pwr : Abilities::Powers) - if (pwr.skillType == Abilities::SkillTable[skillID].skillType) - pwr.handle(mob->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]); - - for (NPCEvent& event : NPCManager::NPCEvents) // trigger an ON_COMBAT - if (event.trigger == ON_COMBAT && event.npcType == mob->type) - event.handler(sock, mob); -} - 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); @@ -438,52 +412,52 @@ static void drainMobHP(Mob *mob, int amount) { NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); if (mob->hp <= 0) - Combat::killMob(mob->target, mob); + mob->transition(AIState::DEAD, mob->target); } -static void deadStep(Mob *mob, time_t currTime) { +void Mob::deadStep(time_t currTime) { // despawn the mob after a short delay - if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) { - mob->despawned = true; + if (killedTime != 0 && !despawned && currTime - killedTime > 2000) { + despawned = true; INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt); - pkt.iNPC_ID = mob->id; + pkt.iNPC_ID = id; - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT)); + NPCManager::sendToViewable(this, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT)); // if it was summoned, mark it for removal - if (mob->summoned) { + if (summoned) { std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl; - NPCManager::queueNPCRemoval(mob->id); + NPCManager::queueNPCRemoval(id); return; } // pre-set spawn coordinates if not marked for removal - mob->x = mob->spawnX; - mob->y = mob->spawnY; - mob->z = mob->spawnZ; + x = spawnX; + y = spawnY; + z = spawnZ; } // to guide their groupmates, group leaders still need to move despite being dead - if (mob->groupLeader == mob->id) - roamingStep(mob, currTime); + if (groupLeader == id) + roamingStep(currTime); - if (mob->killedTime != 0 && currTime - mob->killedTime < mob->regenTime * 100) + if (killedTime != 0 && currTime - killedTime < regenTime * 100) return; - std::cout << "respawning mob " << mob->id << " with HP = " << mob->maxHealth << std::endl; + std::cout << "respawning mob " << id << " with HP = " << maxHealth << std::endl; - mob->hp = mob->maxHealth; - mob->state = MobState::ROAMING; + hp = maxHealth; + state = AIState::ROAMING; // if mob is a group leader/follower, spawn where the group is. - if (mob->groupLeader != 0) { - if (NPCManager::NPCs.find(mob->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[mob->groupLeader]->kind == EntityType::MOB) { - Mob* leaderMob = (Mob*)NPCManager::NPCs[mob->groupLeader]; - mob->x = leaderMob->x + mob->offsetX; - mob->y = leaderMob->y + mob->offsetY; - mob->z = leaderMob->z; + if (groupLeader != 0) { + if (NPCManager::NPCs.find(groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[groupLeader]->kind == EntityType::MOB) { + Mob* leaderMob = (Mob*)NPCManager::NPCs[groupLeader]; + x = leaderMob->x + offsetX; + y = leaderMob->y + offsetY; + z = leaderMob->z; } else { std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl; } @@ -491,127 +465,116 @@ static void deadStep(Mob *mob, time_t currTime) { INITSTRUCT(sP_FE2CL_NPC_NEW, pkt); - pkt.NPCAppearanceData = mob->getAppearanceData(); + pkt.NPCAppearanceData = getAppearanceData(); // notify all nearby players - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW)); + NPCManager::sendToViewable(this, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW)); } -static void combatStep(Mob *mob, time_t currTime) { - assert(mob->target != nullptr); +void Mob::combatStep(time_t currTime) { + assert(target != nullptr); // lose aggro if the player lost connection - if (PlayerManager::players.find(mob->target) == PlayerManager::players.end()) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!aggroCheck(mob, currTime)) { - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); - } + if (PlayerManager::players.find(target) == PlayerManager::players.end()) { + if (!MobAI::aggroCheck(this, getTime())) + transition(AIState::RETREAT, target); return; } - Player *plr = PlayerManager::getPlayer(mob->target); + Player *plr = PlayerManager::getPlayer(target); // lose aggro if the player became invulnerable or died if (plr->HP <= 0 || (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVULNERABLE)) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!aggroCheck(mob, currTime)) { - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); - } + if (!MobAI::aggroCheck(this, getTime())) + transition(AIState::RETREAT, target); return; } // drain - if (mob->skillStyle < 0 && (mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000) - && mob->cbf & CSB_BIT_BOUNDINGBALL) { - drainMobHP(mob, mob->maxHealth / 20); // lose 5% every second - mob->lastDrainTime = currTime; + if (skillStyle < 0 && (lastDrainTime == 0 || currTime - lastDrainTime >= 1000) + && cbf & CSB_BIT_BOUNDINGBALL) { + drainMobHP(this, maxHealth / 20); // lose 5% every second + lastDrainTime = currTime; } // if drain killed the mob, return early - if (mob->hp <= 0) + if (hp <= 0) return; // unbuffing - std::unordered_map::iterator it = mob->unbuffTimes.begin(); - while (it != mob->unbuffTimes.end()) { + std::unordered_map::iterator it = unbuffTimes.begin(); + while (it != unbuffTimes.end()) { if (currTime >= it->second) { - mob->cbf &= ~it->first; + cbf &= ~it->first; INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); pkt1.eCT = 2; - pkt1.iID = mob->id; - pkt1.iConditionBitFlag = mob->cbf; - NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); + pkt1.iID = id; + pkt1.iConditionBitFlag = cbf; + NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); - it = mob->unbuffTimes.erase(it); + it = unbuffTimes.erase(it); } else { it++; } } // skip attack if stunned or asleep - if (mob->cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) { - mob->skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up. + if (cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) { + skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up. return; } - int distance = hypot(plr->x - mob->x, plr->y - mob->y); - int mobRange = (int)mob->data["m_iAtkRange"] + (int)mob->data["m_iRadius"]; + int distance = hypot(plr->x - x, plr->y - y); + int mobRange = (int)data["m_iAtkRange"] + (int)data["m_iRadius"]; - if (currTime >= mob->nextAttack) { - if (mob->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance. - useAbilities(mob, currTime); - if (mob->target == nullptr) + if (currTime >= nextAttack) { + if (skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance. + useAbilities(this, currTime); + if (target == nullptr) return; } int distanceToTravel = INT_MAX; - int speed = mob->speed; // movement logic: move when out of range but don't move while casting a skill - if (distance > mobRange && mob->skillStyle == -1) { - if (mob->nextMovement != 0 && currTime < mob->nextMovement) + if (distance > mobRange && skillStyle == -1) { + if (nextMovement != 0 && currTime < nextMovement) return; - mob->nextMovement = currTime + 400; - if (currTime >= mob->nextAttack) - mob->nextAttack = 0; + nextMovement = currTime + 400; + if (currTime >= nextAttack) + nextAttack = 0; // halve movement speed if snared - if (mob->cbf & CSB_BIT_DN_MOVE_SPEED) + if (cbf & CSB_BIT_DN_MOVE_SPEED) speed /= 2; int targetX = plr->x; int targetY = plr->y; - if (mob->groupLeader != 0) { - targetX += mob->offsetX*distance/(mob->idleRange + 1); - targetY += mob->offsetY*distance/(mob->idleRange + 1); + if (groupLeader != 0) { + targetX += offsetX*distance/(idleRange + 1); + targetY += offsetY*distance/(idleRange + 1); } distanceToTravel = std::min(distance-mobRange+1, speed*2/5); - auto targ = lerp(mob->x, mob->y, targetX, targetY, distanceToTravel); - if (distanceToTravel < speed*2/5 && currTime >= mob->nextAttack) - mob->nextAttack = 0; + auto targ = lerp(x, y, targetX, targetY, distanceToTravel); + if (distanceToTravel < speed*2/5 && currTime >= nextAttack) + nextAttack = 0; - NPCManager::updateNPCPosition(mob->id, targ.first, targ.second, mob->z, mob->instanceID, mob->angle); + NPCManager::updateNPCPosition(id, targ.first, targ.second, z, instanceID, angle); INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); - pkt.iNPC_ID = mob->id; + pkt.iNPC_ID = id; pkt.iSpeed = speed; - pkt.iToX = mob->x = targ.first; - pkt.iToY = mob->y = targ.second; + pkt.iToX = x = targ.first; + pkt.iToY = y = targ.second; pkt.iToZ = plr->z; pkt.iMoveStyle = 1; // notify all nearby players - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); + NPCManager::sendToViewable(this, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); } /* attack logic @@ -619,21 +582,17 @@ static void combatStep(Mob *mob, time_t currTime) { * if the mob is one move interval away, we should just start attacking anyways. */ if (distance <= mobRange || distanceToTravel < speed*2/5) { - if (mob->nextAttack == 0 || currTime >= mob->nextAttack) { - mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100; - Combat::npcAttackPc(mob, currTime); + if (nextAttack == 0 || currTime >= nextAttack) { + nextAttack = currTime + (int)data["m_iDelayTime"] * 100; + Combat::npcAttackPc(this, currTime); } } // retreat if the player leaves combat range - int xyDistance = hypot(plr->x - mob->roamX, plr->y - mob->roamY); - distance = hypot(xyDistance, plr->z - mob->roamZ); - if (distance >= mob->data["m_iCombatRange"]) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); + int xyDistance = hypot(plr->x - roamX, plr->y - roamY); + distance = hypot(xyDistance, plr->z - roamZ); + if (distance >= data["m_iCombatRange"]) { + transition(AIState::RETREAT, target); } } @@ -645,23 +604,23 @@ void MobAI::incNextMovement(Mob *mob, time_t currTime) { mob->nextMovement = currTime + delay/2 + Rand::rand(delay/2); } -static void roamingStep(Mob *mob, time_t currTime) { +void Mob::roamingStep(time_t currTime) { /* * 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. */ - if (mob->state != MobState::DEAD && (mob->nextAttack == 0 || currTime >= mob->nextAttack)) { - mob->nextAttack = currTime + 500; - if (aggroCheck(mob, currTime)) + if (state != AIState::DEAD && (nextAttack == 0 || currTime >= nextAttack)) { + nextAttack = currTime + 500; + if (aggroCheck(this, currTime)) return; } // no random roaming if the mob already has a set path - if (mob->staticPath) + if (staticPath) return; - if (mob->groupLeader != 0 && mob->groupLeader != mob->id) // don't roam by yourself without group leader + if (groupLeader != 0 && groupLeader != id) // don't roam by yourself without group leader return; /* @@ -669,62 +628,61 @@ static void roamingStep(Mob *mob, time_t currTime) { * Transport::stepNPCPathing() (which ticks at a higher frequency than nextMovement), * so we don't have to check if there's already entries in the queue since we know there won't be. */ - if (mob->nextMovement != 0 && currTime < mob->nextMovement) + if (nextMovement != 0 && currTime < nextMovement) return; - incNextMovement(mob, currTime); + incNextMovement(this, currTime); - int xStart = mob->spawnX - mob->idleRange/2; - int yStart = mob->spawnY - mob->idleRange/2; - int speed = mob->speed; + int xStart = spawnX - idleRange/2; + int yStart = spawnY - idleRange/2; // some mobs don't move (and we mustn't divide/modulus by zero) - if (mob->idleRange == 0 || speed == 0) + if (idleRange == 0 || speed == 0) return; int farX, farY, distance; - int minDistance = mob->idleRange / 2; + int minDistance = idleRange / 2; // pick a random destination - farX = xStart + Rand::rand(mob->idleRange); - farY = yStart + Rand::rand(mob->idleRange); + farX = xStart + Rand::rand(idleRange); + farY = yStart + Rand::rand(idleRange); - distance = std::abs(std::max(farX - mob->x, farY - mob->y)); + distance = std::abs(std::max(farX - x, farY - y)); if (distance == 0) distance += 1; // hack to avoid FPE // if it's too short a walk, go further in that direction - farX = mob->x + (farX - mob->x) * minDistance / distance; - farY = mob->y + (farY - mob->y) * minDistance / distance; + farX = x + (farX - x) * minDistance / distance; + farY = y + (farY - y) * minDistance / distance; // but don't got out of bounds - farX = std::clamp(farX, xStart, xStart + mob->idleRange); - farY = std::clamp(farY, yStart, yStart + mob->idleRange); + farX = std::clamp(farX, xStart, xStart + idleRange); + farY = std::clamp(farY, yStart, yStart + idleRange); // halve movement speed if snared - if (mob->cbf & CSB_BIT_DN_MOVE_SPEED) + if (cbf & CSB_BIT_DN_MOVE_SPEED) speed /= 2; std::queue queue; - Vec3 from = { mob->x, mob->y, mob->z }; - Vec3 to = { farX, farY, mob->z }; + Vec3 from = { x, y, z }; + Vec3 to = { farX, farY, z }; // add a route to the queue; to be processed in Transport::stepNPCPathing() Transport::lerp(&queue, from, to, speed); - Transport::NPCQueues[mob->id] = queue; + Transport::NPCQueues[id] = queue; - if (mob->groupLeader != 0 && mob->groupLeader == mob->id) { + if (groupLeader != 0 && groupLeader == id) { // make followers follow this npc. for (int i = 0; i < 4; i++) { - if (mob->groupMember[i] == 0) + if (groupMember[i] == 0) break; - if (NPCManager::NPCs.find(mob->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[mob->groupMember[i]]->kind != EntityType::MOB) { + if (NPCManager::NPCs.find(groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[groupMember[i]]->kind != EntityType::MOB) { std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; continue; } std::queue queue2; - Mob* followerMob = (Mob*)NPCManager::NPCs[mob->groupMember[i]]; + Mob* followerMob = (Mob*)NPCManager::NPCs[groupMember[i]]; from = { followerMob->x, followerMob->y, followerMob->z }; to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->z }; Transport::lerp(&queue2, from, to, speed); @@ -733,78 +691,148 @@ static void roamingStep(Mob *mob, time_t currTime) { } } -static void retreatStep(Mob *mob, time_t currTime) { - if (mob->nextMovement != 0 && currTime < mob->nextMovement) +void Mob::retreatStep(time_t currTime) { + if (nextMovement != 0 && currTime < nextMovement) return; - mob->nextMovement = currTime + 400; + nextMovement = currTime + 400; // distance between spawn point and current location - int distance = hypot(mob->x - mob->roamX, mob->y - mob->roamY); + int distance = hypot(x - roamX, y - roamY); //if (distance > mob->data["m_iIdleRange"]) { if (distance > 10) { INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); - auto targ = lerp(mob->x, mob->y, mob->roamX, mob->roamY, (int)mob->speed*4/5); + auto targ = lerp(x, y, roamX, roamY, (int)speed*4/5); - pkt.iNPC_ID = mob->id; - pkt.iSpeed = (int)mob->speed * 2; - pkt.iToX = mob->x = targ.first; - pkt.iToY = mob->y = targ.second; - pkt.iToZ = mob->z = mob->spawnZ; + pkt.iNPC_ID = id; + pkt.iSpeed = (int)speed * 2; + pkt.iToX = x = targ.first; + pkt.iToY = y = targ.second; + pkt.iToZ = z = spawnZ; pkt.iMoveStyle = 1; // notify all nearby players - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); + NPCManager::sendToViewable(this, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE)); } // if we got there //if (distance <= mob->data["m_iIdleRange"]) { if (distance <= 10) { // retreat back to the spawn point - mob->state = MobState::ROAMING; - mob->hp = mob->maxHealth; - mob->killedTime = 0; - mob->nextAttack = 0; - mob->cbf = 0; + state = AIState::ROAMING; + hp = maxHealth; + killedTime = 0; + nextAttack = 0; + cbf = 0; // cast a return home heal spell, this is the right way(tm) std::vector targetData = {1, 0, 0, 0, 0}; for (auto& pwr : Abilities::Powers) if (pwr.skillType == Abilities::SkillTable[110].skillType) - pwr.handle(mob->id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]); + pwr.handle(id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]); // clear outlying debuffs - clearDebuff(mob); + clearDebuff(this); } } -void MobAI::step(CombatNPC *npc, time_t currTime) { - assert(npc->kind == EntityType::MOB); - auto mob = (Mob*)npc; +void Mob::onInactive() { + // no-op +} - if (mob->playersInView < 0) - std::cout << "[WARN] Weird playerview value " << mob->playersInView << std::endl; +void Mob::onRoamStart() { + // stub +} - // skip mob movement and combat if disabled or not in view - if ((!simulateMobs || mob->playersInView == 0) && mob->state != MobState::DEAD - && mob->state != MobState::RETREAT) +void Mob::onCombatStart(EntityRef src) { + assert(src.type == EntityType::PLAYER); + target = src.sock; + nextMovement = getTime(); + nextAttack = 0; + + roamX = x; + roamY = y; + roamZ = z; + + int skillID = (int)data["m_iPassiveBuff"]; // cast passive + std::vector targetData = { 1, id, 0, 0, 0 }; + for (auto& pwr : Abilities::Powers) + if (pwr.skillType == Abilities::SkillTable[skillID].skillType) + pwr.handle(id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]); +} + +void Mob::onRetreat() { + target = nullptr; + MobAI::clearDebuff(this); + if (groupLeader != 0) + MobAI::groupRetreat(this); +} + +void Mob::onDeath(EntityRef src) { + target = nullptr; + cbf = 0; + skillStyle = -1; + unbuffTimes.clear(); + killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step? + + // check for the edge case where hitting the mob did not aggro it + if (src.type == EntityType::PLAYER && src.isValid()) { + Player* plr = PlayerManager::getPlayer(src.sock); + + Items::DropRoll rolled; + Items::DropRoll eventRolled; + std::map 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) { + Items::giveMobDrop(src.sock, this, rolled, eventRolled); + Missions::mobKilled(src.sock, type, qitemRolls); + } + 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; + + Items::giveMobDrop(sockTo, this, rolled, eventRolled); + Missions::mobKilled(sockTo, type, qitemRolls); + } + } + } + + // delay the despawn animation + despawned = false; + + auto it = Transport::NPCQueues.find(id); + if (it == Transport::NPCQueues.end() || it->second.empty()) return; - switch (mob->state) { - case MobState::INACTIVE: - // no-op - break; - case MobState::ROAMING: - roamingStep(mob, currTime); - break; - case MobState::COMBAT: - combatStep(mob, currTime); - break; - case MobState::RETREAT: - retreatStep(mob, currTime); - break; - case MobState::DEAD: - deadStep(mob, currTime); - break; + // rewind or empty the movement queue + if (staticPath) { + /* + * 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; + for (auto point = queue.front(); point.x != spawnX || point.y != spawnY; point = queue.front()) { + queue.pop(); + queue.push(point); + } + } + else { + Transport::NPCQueues.erase(id); } } diff --git a/src/MobAI.hpp b/src/MobAI.hpp index 5eb3db8..dbb267b 100644 --- a/src/MobAI.hpp +++ b/src/MobAI.hpp @@ -2,24 +2,10 @@ #include "core/Core.hpp" #include "NPCManager.hpp" - -enum class MobState { - INACTIVE, - ROAMING, - COMBAT, - RETREAT, - DEAD -}; - -namespace MobAI { - // needs to be declared before Mob's constructor - void step(CombatNPC*, time_t); -}; +#include "Entities.hpp" struct Mob : public CombatNPC { // general - MobState state = MobState::INACTIVE; - std::unordered_map unbuffTimes = {}; // dead @@ -47,16 +33,13 @@ struct Mob : public CombatNPC { int offsetX = 0, offsetY = 0; int groupMember[4] = {}; - // for optimizing away AI in empty chunks - int playersInView = 0; - // temporary; until we're sure what's what nlohmann::json data = {}; Mob(int x, int y, int z, int angle, uint64_t iID, int t, nlohmann::json d, int32_t id) : CombatNPC(x, y, z, angle, iID, t, id, d["m_iHP"]), sightRange(d["m_iSightRange"]) { - state = MobState::ROAMING; + state = AIState::ROAMING; data = d; @@ -78,7 +61,6 @@ struct Mob : public CombatNPC { hp = maxHealth; kind = EntityType::MOB; - _stepAI = MobAI::step; } // constructor for /summon @@ -89,6 +71,17 @@ struct Mob : public CombatNPC { ~Mob() {} + virtual void roamingStep(time_t currTime) override; + virtual void combatStep(time_t currTime) override; + virtual void retreatStep(time_t currTime) override; + virtual void deadStep(time_t currTime) override; + + virtual void onInactive() override; + virtual void onRoamStart() override; + virtual void onCombatStart(EntityRef src) override; + virtual void onRetreat() override; + virtual void onDeath(EntityRef src) override; + auto operator[](std::string s) { return data[s]; } @@ -103,5 +96,4 @@ namespace MobAI { void clearDebuff(Mob *mob); void followToCombat(Mob *mob); void groupRetreat(Mob *mob); - void enterCombat(CNSocket *sock, Mob *mob); } diff --git a/src/NPCManager.cpp b/src/NPCManager.cpp index 92c0f2a..990d2d1 100644 --- a/src/NPCManager.cpp +++ b/src/NPCManager.cpp @@ -355,7 +355,7 @@ static void step(CNServer *serv, time_t currTime) { continue; auto npc = (CombatNPC*)pair.second; - npc->stepAI(currTime); + npc->step(currTime); } // deallocate all NPCs queued for removal diff --git a/src/Player.hpp b/src/Player.hpp index 0c425d1..b0c970a 100644 --- a/src/Player.hpp +++ b/src/Player.hpp @@ -11,7 +11,7 @@ #define PC_MAXHEALTH(level) (925 + 75 * (level)) -struct Player : public Entity { +struct Player : public Entity, public ICombatant { int accountId = 0; int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums) int32_t iID = 0; @@ -90,5 +90,13 @@ struct Player : public Entity { virtual void enterIntoViewOf(CNSocket *sock) override; virtual void disappearFromViewOf(CNSocket *sock) override; + virtual int takeDamage(EntityRef src, int amt) override; + virtual void heal(EntityRef src, int amt) override; + virtual bool isAlive() override; + virtual int getCurrentHP() override; + virtual int32_t getID() override; + + virtual void step(time_t currTime) override; + sPCAppearanceData getAppearanceData(); }; diff --git a/src/Transport.cpp b/src/Transport.cpp index 36b0de6..9fe2ccc 100644 --- a/src/Transport.cpp +++ b/src/Transport.cpp @@ -263,7 +263,7 @@ static void stepNPCPathing() { } // do not roam if not roaming - if (npc->kind == EntityType::MOB && ((Mob*)npc)->state != MobState::ROAMING) { + if (npc->kind == EntityType::MOB && ((Mob*)npc)->state != AIState::ROAMING) { it++; continue; } diff --git a/src/core/Packets.hpp b/src/core/Packets.hpp index 55a0358..f79bb2a 100644 --- a/src/core/Packets.hpp +++ b/src/core/Packets.hpp @@ -47,8 +47,8 @@ struct PacketDesc { * really should. */ struct sGM_PVPTarget { - uint32_t eCT; uint32_t iID; + uint32_t eCT; }; struct sSkillResult_Leech {