diff --git a/Makefile b/Makefile index 987749b..d5a5e97 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,8 @@ CXXSRC=\ src/Defines.cpp\ src/main.cpp\ src/MissionManager.cpp\ - src/MobManager.cpp\ + src/MobAI.cpp\ + src/Combat.cpp\ src/NanoManager.cpp\ src/Abilities.cpp\ src/ItemManager.cpp\ @@ -82,7 +83,8 @@ CXXHDR=\ vendor/INIReader.hpp\ vendor/JSON.hpp\ src/MissionManager.hpp\ - src/MobManager.hpp\ + src/MobAI.hpp\ + src/Combat.hpp\ src/NanoManager.hpp\ src/Abilities.hpp\ src/ItemManager.hpp\ diff --git a/src/Abilities.cpp b/src/Abilities.cpp index 4237c0d..6b0ed96 100644 --- a/src/Abilities.cpp +++ b/src/Abilities.cpp @@ -136,13 +136,13 @@ int NanoManager::applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int3 namespace NanoManager { bool doDebuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (MobManager::Mobs.find(targetID) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(targetID) == MobAI::Mobs.end()) { std::cout << "[WARN] doDebuff: mob ID not found" << std::endl; return false; } - Mob* mob = MobManager::Mobs[targetID]; - MobManager::hitMob(sock, mob, 0); + Mob* mob = MobAI::Mobs[targetID]; + Combat::hitMob(sock, mob, 0); respdata[i].eCT = 4; respdata[i].iID = mob->appearanceData.iNPC_ID; @@ -200,15 +200,15 @@ bool doBuff(CNSocket *sock, sSkillResult_Buff *respdata, int i, int32_t targetID } bool doDamageNDebuff(CNSocket *sock, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (MobManager::Mobs.find(targetID) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(targetID) == MobAI::Mobs.end()) { // not sure how to best handle this std::cout << "[WARN] doDamageNDebuff: mob ID not found" << std::endl; return false; } - Mob* mob = MobManager::Mobs[targetID]; + Mob* mob = MobAI::Mobs[targetID]; - MobManager::hitMob(sock, mob, 0); // just to gain aggro + Combat::hitMob(sock, mob, 0); // just to gain aggro respdata[i].eCT = 4; respdata[i].iDamage = duration / 10; @@ -262,16 +262,16 @@ bool doHeal(CNSocket *sock, sSkillResult_Heal_HP *respdata, int i, int32_t targe } bool doDamage(CNSocket *sock, sSkillResult_Damage *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (MobManager::Mobs.find(targetID) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(targetID) == MobAI::Mobs.end()) { // not sure how to best handle this std::cout << "[WARN] doDamage: mob ID not found" << std::endl; return false; } - Mob* mob = MobManager::Mobs[targetID]; + Mob* mob = MobAI::Mobs[targetID]; Player *plr = PlayerManager::getPlayer(sock); - int damage = MobManager::hitMob(sock, mob, std::max(PC_MAXHEALTH(plr->level) * amount / 1000, mob->maxHealth * amount / 1000)); + int damage = Combat::hitMob(sock, mob, std::max(PC_MAXHEALTH(plr->level) * amount / 1000, mob->maxHealth * amount / 1000)); respdata[i].eCT = 4; respdata[i].iDamage = damage; @@ -312,14 +312,14 @@ bool doLeech(CNSocket *sock, sSkillResult_Heal_HP *healdata, int i, int32_t targ healdata->iHP = plr->HP; healdata->iHealHP = healedAmount; - if (MobManager::Mobs.find(targetID) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(targetID) == MobAI::Mobs.end()) { // not sure how to best handle this std::cout << "[WARN] doLeech: mob ID not found" << std::endl; return false; } - Mob* mob = MobManager::Mobs[targetID]; + Mob* mob = MobAI::Mobs[targetID]; - int damage = MobManager::hitMob(sock, mob, amount * 2); + int damage = Combat::hitMob(sock, mob, amount * 2); damagedata->eCT = 4; damagedata->iDamage = damage; @@ -481,7 +481,7 @@ std::vector NanoPowers = { #pragma endregion #pragma region Mob Powers -namespace MobManager { +namespace Combat { bool doDamageNDebuff(Mob *mob, sSkillResult_Damage_N_Debuff *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { CNSocket *sock = nullptr; Player *plr = nullptr; @@ -528,10 +528,10 @@ bool doDamageNDebuff(Mob *mob, sSkillResult_Damage_N_Debuff *respdata, int i, in if (plr->HP <= 0) { mob->target = nullptr; mob->state = MobState::RETREAT; - if (!aggroCheck(mob, getTime())) { - clearDebuff(mob); + if (!MobAI::aggroCheck(mob, getTime())) { + MobAI::clearDebuff(mob); if (mob->groupLeader != 0) - groupRetreat(mob); + MobAI::groupRetreat(mob); } } @@ -539,12 +539,12 @@ bool doDamageNDebuff(Mob *mob, sSkillResult_Damage_N_Debuff *respdata, int i, in } bool doHeal(Mob *mob, sSkillResult_Heal_HP *respdata, int i, int32_t targetID, int32_t bitFlag, int16_t timeBuffID, int16_t duration, int16_t amount) { - if (MobManager::Mobs.find(targetID) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(targetID) == MobAI::Mobs.end()) { std::cout << "[WARN] doDebuff: mob ID not found" << std::endl; return false; } - Mob* targetMob = MobManager::Mobs[targetID]; + Mob* targetMob = MobAI::Mobs[targetID]; int healedAmount = amount * targetMob->maxHealth / 1000; targetMob->appearanceData.iHP += healedAmount; @@ -602,10 +602,10 @@ bool doDamage(Mob *mob, sSkillResult_Damage *respdata, int i, int32_t targetID, if (plr->HP <= 0) { mob->target = nullptr; mob->state = MobState::RETREAT; - if (!aggroCheck(mob, getTime())) { - clearDebuff(mob); + if (!MobAI::aggroCheck(mob, getTime())) { + MobAI::clearDebuff(mob); if (mob->groupLeader != 0) - groupRetreat(mob); + MobAI::groupRetreat(mob); } } @@ -660,10 +660,10 @@ bool doLeech(Mob *mob, sSkillResult_Heal_HP *healdata, int i, int32_t targetID, if (plr->HP <= 0) { mob->target = nullptr; mob->state = MobState::RETREAT; - if (!aggroCheck(mob, getTime())) { - clearDebuff(mob); + if (!MobAI::aggroCheck(mob, getTime())) { + MobAI::clearDebuff(mob); if (mob->groupLeader != 0) - groupRetreat(mob); + MobAI::groupRetreat(mob); } } diff --git a/src/Abilities.hpp b/src/Abilities.hpp index 4599676..439093d 100644 --- a/src/Abilities.hpp +++ b/src/Abilities.hpp @@ -1,7 +1,7 @@ #pragma once #include "CNProtocol.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" typedef void (*PowerHandler)(CNSocket*, std::vector, int16_t, int16_t, int16_t, int16_t, int16_t, int32_t, int16_t); @@ -57,6 +57,6 @@ namespace NanoManager { int applyBuff(CNSocket* sock, int skillID, int eTBU, int eTBT, int32_t groupFlags); } -namespace MobManager { +namespace Combat { extern std::vector MobPowers; } diff --git a/src/CNShardServer.cpp b/src/CNShardServer.cpp index 4f9a2a4..ae85b84 100644 --- a/src/CNShardServer.cpp +++ b/src/CNShardServer.cpp @@ -2,7 +2,7 @@ #include "CNStructs.hpp" #include "CNShardServer.hpp" #include "PlayerManager.hpp" -#include "MobManager.hpp" +#include "MobAI.hpp" #include "CNShared.hpp" #include "settings.hpp" #include "Database.hpp" diff --git a/src/ChunkManager.cpp b/src/ChunkManager.cpp index 3038db7..567064b 100644 --- a/src/ChunkManager.cpp +++ b/src/ChunkManager.cpp @@ -2,7 +2,7 @@ #include "PlayerManager.hpp" #include "NPCManager.hpp" #include "settings.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" std::map ChunkManager::chunks; @@ -465,7 +465,7 @@ void ChunkManager::createInstance(uint64_t instanceID) { Mob* newMob = new Mob(baseNPC->appearanceData.iX, baseNPC->appearanceData.iY, baseNPC->appearanceData.iZ, baseNPC->appearanceData.iAngle, instanceID, baseNPC->appearanceData.iNPCType, NPCManager::NPCData[baseNPC->appearanceData.iNPCType], NPCManager::nextId++); NPCManager::NPCs[newMob->appearanceData.iNPC_ID] = newMob; - MobManager::Mobs[newMob->appearanceData.iNPC_ID] = newMob; + MobAI::Mobs[newMob->appearanceData.iNPC_ID] = newMob; // if in a group, copy over group members as well if (((Mob*)baseNPC)->groupLeader != 0) { @@ -480,7 +480,7 @@ void ChunkManager::createInstance(uint64_t instanceID) { instanceID, baseFollower->appearanceData.iNPCType, NPCManager::NPCData[baseFollower->appearanceData.iNPCType], followerID); // add follower to NPC maps NPCManager::NPCs[followerID] = newMobFollower; - MobManager::Mobs[followerID] = newMobFollower; + MobAI::Mobs[followerID] = newMobFollower; // set follower-specific properties newMobFollower->groupLeader = newMob->appearanceData.iNPC_ID; newMobFollower->offsetX = ((Mob*)baseFollower)->offsetX; diff --git a/src/MobManager.cpp b/src/Combat.cpp similarity index 51% rename from src/MobManager.cpp rename to src/Combat.cpp index bf8d7b2..9a46c7c 100644 --- a/src/MobManager.cpp +++ b/src/Combat.cpp @@ -1,4 +1,4 @@ -#include "MobManager.hpp" +#include "Combat.hpp" #include "PlayerManager.hpp" #include "NanoManager.hpp" #include "NPCManager.hpp" @@ -9,22 +9,14 @@ #include "RacingManager.hpp" #include "Abilities.hpp" -#include -#include #include -std::map MobManager::Mobs; -std::queue MobManager::RemovalQueue; - -std::map MobManager::MobDropChances; -std::map MobManager::MobDrops; +std::map Combat::MobDropChances; +std::map Combat::MobDrops; /// Player Id -> Bullet Id -> Bullet -std::map> MobManager::Bullets; +std::map> Combat::Bullets; -bool MobManager::simulateMobs; - -void MobManager::init() { - REGISTER_SHARD_TIMER(step, 200); +void Combat::init() { REGISTER_SHARD_TIMER(playerTick, 2000); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs); @@ -37,11 +29,9 @@ void MobManager::init() { REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE, grenadeFire); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE, rocketFire); REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ROCKET_STYLE_HIT, projectileHit); - - simulateMobs = settings::SIMULATEMOBS; } -void MobManager::pcAttackNpcs(CNSocket *sock, CNPacketData *data) { +void Combat::pcAttackNpcs(CNSocket *sock, CNPacketData *data) { sP_CL2FE_REQ_PC_ATTACK_NPCs* pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf; Player *plr = PlayerManager::getPlayer(sock); @@ -90,12 +80,12 @@ void MobManager::pcAttackNpcs(CNSocket *sock, CNPacketData *data) { resp->iNPCCnt = pkt->iNPCCnt; for (int i = 0; i < pkt->iNPCCnt; i++) { - if (Mobs.find(pktdata[i]) == Mobs.end()) { + if (MobAI::Mobs.find(pktdata[i]) == MobAI::Mobs.end()) { // not sure how to best handle this std::cout << "[WARN] pcAttackNpcs: mob ID not found" << std::endl; return; } - Mob *mob = Mobs[pktdata[i]]; + Mob *mob = MobAI::Mobs[pktdata[i]]; std::pair damage; @@ -133,7 +123,7 @@ void MobManager::pcAttackNpcs(CNSocket *sock, CNPacketData *data) { PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_ATTACK_NPCs, resplen); } -void MobManager::npcAttackPc(Mob *mob, time_t currTime) { +void Combat::npcAttackPc(Mob *mob, time_t currTime) { Player *plr = PlayerManager::getPlayer(mob->target); const size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) + sizeof(sAttackResult); @@ -163,15 +153,15 @@ void MobManager::npcAttackPc(Mob *mob, time_t currTime) { if (plr->HP <= 0) { mob->target = nullptr; mob->state = MobState::RETREAT; - if (!aggroCheck(mob, currTime)) { - clearDebuff(mob); + if (!MobAI::aggroCheck(mob, currTime)) { + MobAI::clearDebuff(mob); if (mob->groupLeader != 0) - groupRetreat(mob); + MobAI::groupRetreat(mob); } } } -void MobManager::giveReward(CNSocket *sock, Mob* mob, int rolledBoosts, int rolledPotions, +void Combat::giveReward(CNSocket *sock, Mob* mob, int rolledBoosts, int rolledPotions, int rolledCrate, int rolledCrateType, int rolledEvent) { Player *plr = PlayerManager::getPlayer(sock); @@ -275,7 +265,7 @@ void MobManager::giveReward(CNSocket *sock, Mob* mob, int rolledBoosts, int roll giveEventReward(sock, plr, rolledEvent); } -void MobManager::getReward(sItemBase *reward, MobDrop* drop, MobDropChance* chance, int rolled) { +void Combat::getReward(sItemBase *reward, MobDrop* drop, MobDropChance* chance, int rolled) { reward->iType = 9; reward->iOpt = 1; @@ -295,7 +285,7 @@ void MobManager::getReward(sItemBase *reward, MobDrop* drop, MobDropChance* chan while (sum<=randomNum); } -void MobManager::giveEventReward(CNSocket* sock, Player* player, int rolled) { +void Combat::giveEventReward(CNSocket* sock, Player* player, int rolled) { // random drop chance if (rand() % 100 > settings::EVENTCRATECHANCE) return; @@ -350,7 +340,7 @@ void MobManager::giveEventReward(CNSocket* sock, Player* player, int rolled) { sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen); } -int MobManager::hitMob(CNSocket *sock, Mob *mob, int damage) { +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 @@ -361,10 +351,10 @@ int MobManager::hitMob(CNSocket *sock, Mob *mob, int damage) { if (mob->state == MobState::ROAMING) { assert(mob->target == nullptr); - enterCombat(sock, mob); + MobAI::enterCombat(sock, mob); if (mob->groupLeader != 0) - followToCombat(mob); + MobAI::followToCombat(mob); } mob->appearanceData.iHP -= damage; @@ -386,7 +376,7 @@ int MobManager::hitMob(CNSocket *sock, Mob *mob, int damage) { return damage; } -void MobManager::killMob(CNSocket *sock, Mob *mob) { +void Combat::killMob(CNSocket *sock, Mob *mob) { mob->state = MobState::DEAD; mob->target = nullptr; mob->appearanceData.iConditionBitFlag = 0; @@ -462,409 +452,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) { } } -void MobManager::deadStep(Mob *mob, time_t currTime) { - // despawn the mob after a short delay - if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) { - mob->despawned = true; - - INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt); - - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; - - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT)); - - // if it was summoned, mark it for removal - if (mob->summoned) { - std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl; - RemovalQueue.push(mob->appearanceData.iNPC_ID); - return; - } - - // pre-set spawn coordinates if not marked for removal - mob->appearanceData.iX = mob->spawnX; - mob->appearanceData.iY = mob->spawnY; - mob->appearanceData.iZ = mob->spawnZ; - } - - // to guide their groupmates, group leaders still need to move despite being dead - if (mob->groupLeader == mob->appearanceData.iNPC_ID) - roamingStep(mob, currTime); - - if (mob->killedTime != 0 && currTime - mob->killedTime < mob->regenTime * 100) - return; - - std::cout << "respawning mob " << mob->appearanceData.iNPC_ID << " with HP = " << mob->maxHealth << std::endl; - - mob->appearanceData.iHP = mob->maxHealth; - mob->state = MobState::ROAMING; - - // if mob is a group leader/follower, spawn where the group is. - if (mob->groupLeader != 0) { - if (Mobs.find(mob->groupLeader) != Mobs.end()) { - Mob* leaderMob = Mobs[mob->groupLeader]; - mob->appearanceData.iX = leaderMob->appearanceData.iX + mob->offsetX; - mob->appearanceData.iY = leaderMob->appearanceData.iY + mob->offsetY; - mob->appearanceData.iZ = leaderMob->appearanceData.iZ; - } else { - std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl; - } - } - - INITSTRUCT(sP_FE2CL_NPC_NEW, pkt); - - pkt.NPCAppearanceData = mob->appearanceData; - - // notify all nearby players - NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW)); -} - -void MobManager::combatStep(Mob *mob, time_t currTime) { - assert(mob->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); - } - return; - } - - Player *plr = PlayerManager::getPlayer(mob->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); - } - return; - } - - // drain - if (mob->skillStyle < 0 && (mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000) - && mob->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) { - drainMobHP(mob, mob->maxHealth / 20); // lose 5% every second - mob->lastDrainTime = currTime; - } - - // if drain killed the mob, return early - if (mob->appearanceData.iHP <= 0) - return; - - // unbuffing - std::unordered_map::iterator it = mob->unbuffTimes.begin(); - while (it != mob->unbuffTimes.end()) { - - if (currTime >= it->second) { - mob->appearanceData.iConditionBitFlag &= ~it->first; - - INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); - pkt1.eCT = 2; - pkt1.iID = mob->appearanceData.iNPC_ID; - pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag; - NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); - - it = mob->unbuffTimes.erase(it); - } else { - it++; - } - } - - // skip attack if stunned or asleep - if (mob->appearanceData.iConditionBitFlag & (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. - return; - } - - int distance = hypot(plr->x - mob->appearanceData.iX, plr->y - mob->appearanceData.iY); - int mobRange = (int)mob->data["m_iAtkRange"] + (int)mob->data["m_iRadius"]; - - if (currTime >= mob->nextAttack) { - if (mob->skillStyle != -1 || distance <= mobRange || rand() % 20 == 0) // while not in attack range, 1 / 20 chance. - useAbilities(mob, currTime); - if (mob->target == nullptr) - return; - } - - int distanceToTravel = INT_MAX; - int speed = mob->data["m_iRunSpeed"]; - // 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) - return; - mob->nextMovement = currTime + 400; - if (currTime >= mob->nextAttack) - mob->nextAttack = 0; - - // halve movement speed if snared - if (mob->appearanceData.iConditionBitFlag & 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); - } - - distanceToTravel = std::min(distance-mobRange+1, speed*2/5); - auto targ = lerp(mob->appearanceData.iX, mob->appearanceData.iY, targetX, targetY, distanceToTravel); - if (distanceToTravel < speed*2/5 && currTime >= mob->nextAttack) - mob->nextAttack = 0; - - NPCManager::updateNPCPosition(mob->appearanceData.iNPC_ID, targ.first, targ.second, mob->appearanceData.iZ, mob->instanceID, mob->appearanceData.iAngle); - - INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); - - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; - pkt.iSpeed = speed; - pkt.iToX = mob->appearanceData.iX = targ.first; - pkt.iToY = mob->appearanceData.iY = 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)); - } - - /* attack logic - * 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. - */ - if (distance <= mobRange || distanceToTravel < speed*2/5) { - if (mob->nextAttack == 0 || currTime >= mob->nextAttack) { - mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100; - npcAttackPc(mob, 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); - } -} - -void MobManager::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() % (delay/2); -} - -void MobManager::roamingStep(Mob *mob, 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)) - return; - } - - // no random roaming if the mob already has a set path - if (mob->staticPath) - return; - - if (mob->groupLeader != 0 && mob->groupLeader != mob->appearanceData.iNPC_ID) // don't roam by yourself without group leader - return; - - /* - * mob->nextMovement is also updated whenever the path queue is traversed in - * TransportManager::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) - return; - incNextMovement(mob, currTime); - - int xStart = mob->spawnX - mob->idleRange/2; - int yStart = mob->spawnY - mob->idleRange/2; - int speed = mob->data["m_iWalkSpeed"]; - - // some mobs don't move (and we mustn't divide/modulus by zero) - if (mob->idleRange == 0 || speed == 0) - return; - - int farX, farY, distance; - int minDistance = mob->idleRange / 2; - - // pick a random destination - farX = xStart + rand() % mob->idleRange; - farY = yStart + rand() % mob->idleRange; - - distance = std::abs(std::max(farX - mob->appearanceData.iX, farY - mob->appearanceData.iY)); - if (distance == 0) - distance += 1; // hack to avoid FPE - - // if it's too short a walk, go further in that direction - farX = mob->appearanceData.iX + (farX - mob->appearanceData.iX) * minDistance / distance; - farY = mob->appearanceData.iY + (farY - mob->appearanceData.iY) * 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); - - // halve movement speed if snared - if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED) - speed /= 2; - - std::queue queue; - WarpLocation from = { mob->appearanceData.iX, mob->appearanceData.iY, mob->appearanceData.iZ }; - WarpLocation to = { farX, farY, mob->appearanceData.iZ }; - - // add a route to the queue; to be processed in TransportManager::stepNPCPathing() - TransportManager::lerp(&queue, from, to, speed); - TransportManager::NPCQueues[mob->appearanceData.iNPC_ID] = queue; - - if (mob->groupLeader != 0 && mob->groupLeader == mob->appearanceData.iNPC_ID) { - // make followers follow this npc. - for (int i = 0; i < 4; i++) { - if (mob->groupMember[i] == 0) - break; - - if (Mobs.find(mob->groupMember[i]) == Mobs.end()) { - std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; - continue; - } - - std::queue queue2; - Mob* followerMob = Mobs[mob->groupMember[i]]; - from = { followerMob->appearanceData.iX, followerMob->appearanceData.iY, followerMob->appearanceData.iZ }; - to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->appearanceData.iZ }; - TransportManager::lerp(&queue2, from, to, speed); - TransportManager::NPCQueues[followerMob->appearanceData.iNPC_ID] = queue2; - } - } -} - -void MobManager::retreatStep(Mob *mob, time_t currTime) { - if (mob->nextMovement != 0 && currTime < mob->nextMovement) - return; - - mob->nextMovement = currTime + 400; - - // distance between spawn point and current location - int distance = hypot(mob->appearanceData.iX - mob->roamX, mob->appearanceData.iY - mob->roamY); - - //if (distance > mob->data["m_iIdleRange"]) { - if (distance > 10) { - INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); - - auto targ = lerp(mob->appearanceData.iX, mob->appearanceData.iY, mob->roamX, mob->roamY, (int)mob->data["m_iRunSpeed"]*4/5); - - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; - pkt.iSpeed = (int)mob->data["m_iRunSpeed"] * 2; - pkt.iToX = mob->appearanceData.iX = targ.first; - pkt.iToY = mob->appearanceData.iY = targ.second; - pkt.iToZ = mob->appearanceData.iZ = mob->spawnZ; - pkt.iMoveStyle = 1; - - // notify all nearby players - NPCManager::sendToViewable(mob, &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->appearanceData.iHP = mob->maxHealth; - mob->killedTime = 0; - mob->nextAttack = 0; - mob->appearanceData.iConditionBitFlag = 0; - - // cast a return home heal spell, this is the right way(tm) - std::vector targetData = {1, 0, 0, 0, 0}; - for (auto& pwr : MobPowers) - if (pwr.skillType == NanoManager::SkillTable[110].skillType) - pwr.handle(mob, targetData, 110, NanoManager::SkillTable[110].durationTime[0], NanoManager::SkillTable[110].powerIntensity[0]); - // clear outlying debuffs - clearDebuff(mob); - } -} - -void MobManager::step(CNServer *serv, time_t currTime) { - for (auto& pair : Mobs) { - if (pair.second->playersInView < 0) - std::cout << "[WARN] Weird playerview value " << pair.second->playersInView << std::endl; - - // skip mob movement and combat if disabled or not in view - if ((!simulateMobs || pair.second->playersInView == 0) && pair.second->state != MobState::DEAD - && pair.second->state != MobState::RETREAT) - continue; - - switch (pair.second->state) { - case MobState::INACTIVE: - // no-op - break; - case MobState::ROAMING: - roamingStep(pair.second, currTime); - break; - case MobState::COMBAT: - combatStep(pair.second, currTime); - break; - case MobState::RETREAT: - retreatStep(pair.second, currTime); - break; - case MobState::DEAD: - deadStep(pair.second, currTime); - break; - } - } - - // deallocate all NPCs queued for removal - while (RemovalQueue.size() > 0) { - NPCManager::destroyNPC(RemovalQueue.front()); - RemovalQueue.pop(); - } -} - -/* - * Dynamic lerp; distinct from TransportManager::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. - */ -std::pair MobManager::lerp(int x1, int y1, int x2, int y2, int speed) { - std::pair 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 MobManager::combatBegin(CNSocket *sock, CNPacketData *data) { +void Combat::combatBegin(CNSocket *sock, CNPacketData *data) { Player *plr = PlayerManager::getPlayer(sock); plr->inCombat = true; @@ -879,7 +467,7 @@ void MobManager::combatBegin(CNSocket *sock, CNPacketData *data) { PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_PC_EQUIP_CHANGE, sizeof(sP_FE2CL_PC_EQUIP_CHANGE)); } -void MobManager::combatEnd(CNSocket *sock, CNPacketData *data) { +void Combat::combatEnd(CNSocket *sock, CNPacketData *data) { Player *plr = PlayerManager::getPlayer(sock); if (plr != nullptr) { @@ -888,7 +476,7 @@ void MobManager::combatEnd(CNSocket *sock, CNPacketData *data) { } } -void MobManager::dotDamageOnOff(CNSocket *sock, CNPacketData *data) { +void Combat::dotDamageOnOff(CNSocket *sock, CNPacketData *data) { sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf; Player *plr = PlayerManager::getPlayer(sock); @@ -905,7 +493,7 @@ void MobManager::dotDamageOnOff(CNSocket *sock, CNPacketData *data) { sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE)); } -void MobManager::dealGooDamage(CNSocket *sock, int amount) { +void Combat::dealGooDamage(CNSocket *sock, int amount) { size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_DotDamage); assert(resplen < CN_PACKET_BUFFER_SIZE - 8); uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; @@ -951,7 +539,348 @@ void MobManager::dealGooDamage(CNSocket *sock, int amount) { PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); } -void MobManager::playerTick(CNServer *serv, time_t currTime) { +std::pair Combat::getDamage(int attackPower, int defensePower, bool shouldCrit, + bool batteryBoost, int attackerStyle, + int defenderStyle, int difficulty) { + std::pair ret = {0, 1}; + if (attackPower + defensePower * 2 == 0) + return ret; + + // base calculation + int damage = attackPower * attackPower / (attackPower + defensePower); + damage = std::max(10 + attackPower / 10, damage - (defensePower - attackPower / 6) * difficulty / 100); + damage = damage * (rand() % 40 + 80) / 100; + + // Adaptium/Blastons/Cosmix + if (attackerStyle != -1 && defenderStyle != -1 && attackerStyle != defenderStyle) { + if (attackerStyle - defenderStyle == 2) + defenderStyle += 3; + if (defenderStyle - attackerStyle == 2) + defenderStyle -= 3; + if (attackerStyle < defenderStyle) + damage = damage * 5 / 4; + else + damage = damage * 4 / 5; + } + + // weapon boosts + if (batteryBoost) + damage = damage * 5 / 4; + + ret.first = damage; + ret.second = 1; + + if (shouldCrit && rand() % 20 == 0) { + ret.first *= 2; // critical hit + ret.second = 2; + } + + return ret; +} + +void Combat::pcAttackChars(CNSocket *sock, CNPacketData *data) { + sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf; + Player *plr = PlayerManager::getPlayer(sock); + + // only GMs can use this this variant + if (plr->accountLevel > 30) + 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)) { + std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n"; + return; + } + + int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs)); + + if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) { + std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n"; + return; + } + + // initialize response struct + size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) + pkt->iTargetCnt * sizeof(sAttackResult); + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + + memset(respbuf, 0, resplen); + + sP_FE2CL_PC_ATTACK_CHARs_SUCC *resp = (sP_FE2CL_PC_ATTACK_CHARs_SUCC*)respbuf; + sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC)); + + 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; + + for (auto& pair : PlayerManager::players) { + if (pair.second->iID == pktdata[i*2]) { + target = pair.second; + break; + } + } + + if (target == nullptr) { + // you shall not pass + std::cout << "[WARN] pcAttackChars: player ID not found" << std::endl; + return; + } + + std::pair damage; + + 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 (MobAI::Mobs.find(pktdata[i*2]) == MobAI::Mobs.end()) { + // not sure how to best handle this + std::cout << "[WARN] pcAttackNpcs: mob ID not found" << std::endl; + return; + } + Mob *mob = MobAI::Mobs[pktdata[i*2]]; + + std::pair damage; + + if (pkt->iTargetCnt > 1) + damage.first = plr->groupDamage; + else + damage.first = plr->pointDamage; + + int difficulty = (int)mob->data["m_iNpcLevel"]; + + damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty), + NanoManager::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->appearanceData.iNPC_ID; + respdata[i].iDamage = damage.first; + respdata[i].iHP = mob->appearanceData.iHP; + respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade + } + } + + sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen); + + // a bit of a hack: these are the same size, so we can reuse the response packet + assert(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_CHARs)); + sP_FE2CL_PC_ATTACK_CHARs *resp1 = (sP_FE2CL_PC_ATTACK_CHARs*)respbuf; + + resp1->iPC_ID = plr->iID; + + // send to other players + PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_ATTACK_CHARs, resplen); +} + +void Combat::grenadeFire(CNSocket* sock, CNPacketData* data) { + sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE* grenade = (sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE*)data->buf; + Player* plr = PlayerManager::getPlayer(sock); + + INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp); + resp.iToX = grenade->iToX; + resp.iToY = grenade->iToY; + resp.iToZ = grenade->iToZ; + + resp.iBulletID = addBullet(plr, true); + resp.iBatteryW = plr->batteryW; + + // 1 means grenade + resp.Bullet.iID = 1; + sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC)); + + // send packet to nearby players + INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers); + toOthers.iPC_ID = plr->iID; + toOthers.iToX = resp.iToX; + toOthers.iToY = resp.iToY; + toOthers.iToZ = resp.iToZ; + toOthers.iBulletID = resp.iBulletID; + toOthers.Bullet.iID = resp.Bullet.iID; + + PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE)); +} + +void Combat::rocketFire(CNSocket* sock, CNPacketData* data) { + sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE* rocket = (sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE*)data->buf; + Player* plr = PlayerManager::getPlayer(sock); + + // We should be sending back rocket succ packet, but it doesn't work, and this one works + INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp); + resp.iToX = rocket->iToX; + resp.iToY = rocket->iToY; + // rocket->iToZ is broken, this seems like a good height + resp.iToZ = plr->z + 100; + + resp.iBulletID = addBullet(plr, false); + // we have to send it weapon id + resp.Bullet.iID = plr->Equip[0].iID; + resp.iBatteryW = plr->batteryW; + + sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC)); + + // send packet to nearby players + INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers); + toOthers.iPC_ID = plr->iID; + toOthers.iToX = resp.iToX; + toOthers.iToY = resp.iToY; + toOthers.iToZ = resp.iToZ; + toOthers.iBulletID = resp.iBulletID; + toOthers.Bullet.iID = resp.Bullet.iID; + + PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE)); +} + +int8_t Combat::addBullet(Player* plr, bool isGrenade) { + + int8_t findId = 0; + if (Bullets.find(plr->iID) != Bullets.end()) { + // find first free id + for (; findId < 127; findId++) + if (Bullets[plr->iID].find(findId) == Bullets[plr->iID].end()) + break; + } + + // sanity check + if (findId == 127) { + std::cout << "[WARN] Player has more than 127 active projectiles?!" << std::endl; + findId = 0; + } + + Bullet toAdd; + toAdd.pointDamage = plr->pointDamage; + toAdd.groupDamage = plr->groupDamage; + // for grenade we need to send 1, for rocket - weapon id + toAdd.bulletType = isGrenade ? 1 : plr->Equip[0].iID; + + // temp solution Jade fix plz + toAdd.weaponBoost = plr->batteryW > 0; + if (toAdd.weaponBoost) { + int boostCost = rand() % 11 + 20; + plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost; + } + + Bullets[plr->iID][findId] = toAdd; + return findId; +} + +void Combat::projectileHit(CNSocket* sock, CNPacketData* data) { + sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT* pkt = (sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT*)data->buf; + Player* plr = PlayerManager::getPlayer(sock); + + if (pkt->iTargetCnt == 0) { + Bullets[plr->iID].erase(pkt->iBulletID); + // no targets hit, don't send response + return; + } + + // sanity check + if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT), pkt->iTargetCnt, sizeof(int64_t), data->size)) { + std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT packet size\n"; + return; + } + + // client sends us 8 byters, where last 4 bytes are mob ID, + // we use int64 pointer to move around but have to remember to cast it to int32 + int64_t* pktdata = (int64_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT)); + + /* + * Due to the possibility of multiplication overflow (and regular buffer overflow), + * both incoming and outgoing variable-length packets must be validated, at least if + * the number of trailing structs isn't well known (ie. it's from the client). + */ + if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT), pkt->iTargetCnt, sizeof(sAttackResult))) { + std::cout << "[WARN] bad sP_FE2CL_PC_GRENADE_STYLE_HIT packet size\n"; + return; + } + + // rapid fire anti-cheat + time_t currTime = getTime(); + if (currTime - plr->lastShot < plr->fireRate * 80) + plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing + else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0) + plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing + + plr->lastShot = currTime; + + if (plr->suspicionRating > 10000) // kill the socket when the player is too suspicious + sock->kill(); + + /* + * initialize response struct + * rocket style hit doesn't work properly, so we're always sending this one + */ + + size_t resplen = sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT) + pkt->iTargetCnt * sizeof(sAttackResult); + uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; + + memset(respbuf, 0, resplen); + + sP_FE2CL_PC_GRENADE_STYLE_HIT* resp = (sP_FE2CL_PC_GRENADE_STYLE_HIT*)respbuf; + sAttackResult* respdata = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT)); + + resp->iTargetCnt = pkt->iTargetCnt; + if (Bullets.find(plr->iID) == Bullets.end() || Bullets[plr->iID].find(pkt->iBulletID) == Bullets[plr->iID].end()) { + std::cout << "[WARN] projectileHit: bullet not found" << std::endl; + return; + } + Bullet* bullet = &Bullets[plr->iID][pkt->iBulletID]; + + for (int i = 0; i < pkt->iTargetCnt; i++) { + if (MobAI::Mobs.find(pktdata[i]) == MobAI::Mobs.end()) { + // not sure how to best handle this + std::cout << "[WARN] projectileHit: mob ID not found" << std::endl; + return; + } + + Mob* mob = MobAI::Mobs[pktdata[i]]; + std::pair damage; + + damage.first = pkt->iTargetCnt > 1 ? bullet->groupDamage : bullet->pointDamage; + + int difficulty = (int)mob->data["m_iNpcLevel"]; + damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, NanoManager::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); + + damage.first = hitMob(sock, mob, damage.first); + + respdata[i].iID = mob->appearanceData.iNPC_ID; + respdata[i].iDamage = damage.first; + respdata[i].iHP = mob->appearanceData.iHP; + respdata[i].iHitFlag = damage.second; + } + + resp->iPC_ID = plr->iID; + resp->iBulletID = pkt->iBulletID; + resp->Bullet.iID = bullet->bulletType; + sock->sendPacket((void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen); + PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen); + + Bullets[plr->iID].erase(resp->iBulletID); +} + +void Combat::playerTick(CNServer *serv, time_t currTime) { static time_t lastHealTime = 0; for (auto& pair : PlayerManager::players) { @@ -1032,724 +961,3 @@ void MobManager::playerTick(CNServer *serv, time_t currTime) { if (currTime - lastHealTime >= 4000) lastHealTime = currTime; } - -std::pair MobManager::getDamage(int attackPower, int defensePower, bool shouldCrit, - bool batteryBoost, int attackerStyle, - int defenderStyle, int difficulty) { - std::pair ret = {0, 1}; - if (attackPower + defensePower * 2 == 0) - return ret; - - // base calculation - int damage = attackPower * attackPower / (attackPower + defensePower); - damage = std::max(10 + attackPower / 10, damage - (defensePower - attackPower / 6) * difficulty / 100); - damage = damage * (rand() % 40 + 80) / 100; - - // Adaptium/Blastons/Cosmix - if (attackerStyle != -1 && defenderStyle != -1 && attackerStyle != defenderStyle) { - if (attackerStyle - defenderStyle == 2) - defenderStyle += 3; - if (defenderStyle - attackerStyle == 2) - defenderStyle -= 3; - if (attackerStyle < defenderStyle) - damage = damage * 5 / 4; - else - damage = damage * 4 / 5; - } - - // weapon boosts - if (batteryBoost) - damage = damage * 5 / 4; - - ret.first = damage; - ret.second = 1; - - if (shouldCrit && rand() % 20 == 0) { - ret.first *= 2; // critical hit - ret.second = 2; - } - - return ret; -} - -void MobManager::pcAttackChars(CNSocket *sock, CNPacketData *data) { - sP_CL2FE_REQ_PC_ATTACK_CHARs* pkt = (sP_CL2FE_REQ_PC_ATTACK_CHARs*)data->buf; - Player *plr = PlayerManager::getPlayer(sock); - - // only GMs can use this this variant - if (plr->accountLevel > 30) - 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)) { - std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_CHARs packet size\n"; - return; - } - - int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_CHARs)); - - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC), pkt->iTargetCnt, sizeof(sAttackResult))) { - std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_CHARs_SUCC packet size\n"; - return; - } - - // initialize response struct - size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) + pkt->iTargetCnt * sizeof(sAttackResult); - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - - memset(respbuf, 0, resplen); - - sP_FE2CL_PC_ATTACK_CHARs_SUCC *resp = (sP_FE2CL_PC_ATTACK_CHARs_SUCC*)respbuf; - sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC)); - - 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; - - for (auto& pair : PlayerManager::players) { - if (pair.second->iID == pktdata[i*2]) { - target = pair.second; - break; - } - } - - if (target == nullptr) { - // you shall not pass - std::cout << "[WARN] pcAttackChars: player ID not found" << std::endl; - return; - } - - std::pair damage; - - 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 (Mobs.find(pktdata[i*2]) == Mobs.end()) { - // not sure how to best handle this - std::cout << "[WARN] pcAttackNpcs: mob ID not found" << std::endl; - return; - } - Mob *mob = Mobs[pktdata[i*2]]; - - std::pair damage; - - if (pkt->iTargetCnt > 1) - damage.first = plr->groupDamage; - else - damage.first = plr->pointDamage; - - int difficulty = (int)mob->data["m_iNpcLevel"]; - - damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW > 6 + difficulty), - NanoManager::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->appearanceData.iNPC_ID; - respdata[i].iDamage = damage.first; - respdata[i].iHP = mob->appearanceData.iHP; - respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade - } - } - - sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_CHARs_SUCC, resplen); - - // a bit of a hack: these are the same size, so we can reuse the response packet - assert(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_CHARs)); - sP_FE2CL_PC_ATTACK_CHARs *resp1 = (sP_FE2CL_PC_ATTACK_CHARs*)respbuf; - - resp1->iPC_ID = plr->iID; - - // send to other players - PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_ATTACK_CHARs, resplen); -} - -void MobManager::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)); - - pkt->iID = mob->appearanceData.iNPC_ID; - pkt->eCT = 4; // mob - pkt->iTB_ID = ECSB_BOUNDINGBALL; - - drain->eCT = 4; - drain->iID = mob->appearanceData.iNPC_ID; - drain->iDamage = amount; - drain->iHP = mob->appearanceData.iHP -= amount; - - NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); - - if (mob->appearanceData.iHP <= 0) - killMob(mob->target, mob); -} - -/* - * 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 MobManager::aggroCheck(Mob *mob, time_t currTime) { - CNSocket *closest = nullptr; - int closestDistance = INT_MAX; - - for (auto it = mob->viewableChunks->begin(); it != mob->viewableChunks->end(); it++) { - Chunk* chunk = *it; - for (CNSocket *s : chunk->players) { - Player *plr = PlayerManager::getPlayer(s); - - if (plr->HP <= 0) - continue; - - int mobRange = mob->sightRange; - - if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH - || RacingManager::EPRaces.find(s) != RacingManager::EPRaces.end()) - 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; - - if (mob->state != MobState::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)) - mobRange = -1; - - // height is relevant for aggro distance because of platforming - int xyDistance = hypot(mob->appearanceData.iX - plr->x, mob->appearanceData.iY - plr->y); - int distance = hypot(xyDistance, (mob->appearanceData.iZ - plr->z) * 2); // difference in Z counts twice - - if (distance > mobRange || distance > closestDistance) - continue; - - // found a player - closest = s; - closestDistance = distance; - } - } - - if (closest != nullptr) { - // found closest player. engage. - enterCombat(closest, mob); - - if (mob->groupLeader != 0) - followToCombat(mob); - - return true; - } - - return false; -} - -void MobManager::clearDebuff(Mob *mob) { - mob->skillStyle = -1; - mob->appearanceData.iConditionBitFlag = 0; - mob->unbuffTimes.clear(); - - INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); - pkt1.eCT = 2; - pkt1.iID = mob->appearanceData.iNPC_ID; - pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag; - NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); -} - -void MobManager::grenadeFire(CNSocket* sock, CNPacketData* data) { - sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE* grenade = (sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE*)data->buf; - Player* plr = PlayerManager::getPlayer(sock); - - INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp); - resp.iToX = grenade->iToX; - resp.iToY = grenade->iToY; - resp.iToZ = grenade->iToZ; - - resp.iBulletID = addBullet(plr, true); - resp.iBatteryW = plr->batteryW; - - // 1 means grenade - resp.Bullet.iID = 1; - sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC)); - - // send packet to nearby players - INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers); - toOthers.iPC_ID = plr->iID; - toOthers.iToX = resp.iToX; - toOthers.iToY = resp.iToY; - toOthers.iToZ = resp.iToZ; - toOthers.iBulletID = resp.iBulletID; - toOthers.Bullet.iID = resp.Bullet.iID; - - PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE)); -} - -void MobManager::rocketFire(CNSocket* sock, CNPacketData* data) { - sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE* rocket = (sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE*)data->buf; - Player* plr = PlayerManager::getPlayer(sock); - - // We should be sending back rocket succ packet, but it doesn't work, and this one works - INITSTRUCT(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, resp); - resp.iToX = rocket->iToX; - resp.iToY = rocket->iToY; - // rocket->iToZ is broken, this seems like a good height - resp.iToZ = plr->z + 100; - - resp.iBulletID = addBullet(plr, false); - // we have to send it weapon id - resp.Bullet.iID = plr->Equip[0].iID; - resp.iBatteryW = plr->batteryW; - - sock->sendPacket(&resp, P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, sizeof(sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC)); - - // send packet to nearby players - INITSTRUCT(sP_FE2CL_PC_GRENADE_STYLE_FIRE, toOthers); - toOthers.iPC_ID = plr->iID; - toOthers.iToX = resp.iToX; - toOthers.iToY = resp.iToY; - toOthers.iToZ = resp.iToZ; - toOthers.iBulletID = resp.iBulletID; - toOthers.Bullet.iID = resp.Bullet.iID; - - PlayerManager::sendToViewable(sock, &toOthers, P_FE2CL_PC_GRENADE_STYLE_FIRE, sizeof(sP_FE2CL_PC_GRENADE_STYLE_FIRE)); -} - -int8_t MobManager::addBullet(Player* plr, bool isGrenade) { - - int8_t findId = 0; - if (Bullets.find(plr->iID) != Bullets.end()) { - // find first free id - for (; findId < 127; findId++) - if (Bullets[plr->iID].find(findId) == Bullets[plr->iID].end()) - break; - } - - // sanity check - if (findId == 127) { - std::cout << "[WARN] Player has more than 127 active projectiles?!" << std::endl; - findId = 0; - } - - Bullet toAdd; - toAdd.pointDamage = plr->pointDamage; - toAdd.groupDamage = plr->groupDamage; - // for grenade we need to send 1, for rocket - weapon id - toAdd.bulletType = isGrenade ? 1 : plr->Equip[0].iID; - - // temp solution Jade fix plz - toAdd.weaponBoost = plr->batteryW > 0; - if (toAdd.weaponBoost) { - int boostCost = rand() % 11 + 20; - plr->batteryW = boostCost > plr->batteryW ? 0 : plr->batteryW - boostCost; - } - - Bullets[plr->iID][findId] = toAdd; - return findId; -} - -void MobManager::projectileHit(CNSocket* sock, CNPacketData* data) { - sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT* pkt = (sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT*)data->buf; - Player* plr = PlayerManager::getPlayer(sock); - - if (pkt->iTargetCnt == 0) { - Bullets[plr->iID].erase(pkt->iBulletID); - // no targets hit, don't send response - return; - } - - // sanity check - if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT), pkt->iTargetCnt, sizeof(int64_t), data->size)) { - std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT packet size\n"; - return; - } - - // client sends us 8 byters, where last 4 bytes are mob ID, - // we use int64 pointer to move around but have to remember to cast it to int32 - int64_t* pktdata = (int64_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT)); - - /* - * Due to the possibility of multiplication overflow (and regular buffer overflow), - * both incoming and outgoing variable-length packets must be validated, at least if - * the number of trailing structs isn't well known (ie. it's from the client). - */ - if (!validOutVarPacket(sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT), pkt->iTargetCnt, sizeof(sAttackResult))) { - std::cout << "[WARN] bad sP_FE2CL_PC_GRENADE_STYLE_HIT packet size\n"; - return; - } - - // rapid fire anti-cheat - time_t currTime = getTime(); - if (currTime - plr->lastShot < plr->fireRate * 80) - plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // gain suspicion for rapid firing - else if (currTime - plr->lastShot < plr->fireRate * 180 && plr->suspicionRating > 0) - plr->suspicionRating += plr->fireRate * 100 + plr->lastShot - currTime; // lose suspicion for delayed firing - - plr->lastShot = currTime; - - if (plr->suspicionRating > 10000) // kill the socket when the player is too suspicious - sock->kill(); - - /* - * initialize response struct - * rocket style hit doesn't work properly, so we're always sending this one - */ - - size_t resplen = sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT) + pkt->iTargetCnt * sizeof(sAttackResult); - uint8_t respbuf[CN_PACKET_BUFFER_SIZE]; - - memset(respbuf, 0, resplen); - - sP_FE2CL_PC_GRENADE_STYLE_HIT* resp = (sP_FE2CL_PC_GRENADE_STYLE_HIT*)respbuf; - sAttackResult* respdata = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT)); - - resp->iTargetCnt = pkt->iTargetCnt; - if (Bullets.find(plr->iID) == Bullets.end() || Bullets[plr->iID].find(pkt->iBulletID) == Bullets[plr->iID].end()) { - std::cout << "[WARN] projectileHit: bullet not found" << std::endl; - return; - } - Bullet* bullet = &Bullets[plr->iID][pkt->iBulletID]; - - for (int i = 0; i < pkt->iTargetCnt; i++) { - if (Mobs.find(pktdata[i]) == Mobs.end()) { - // not sure how to best handle this - std::cout << "[WARN] projectileHit: mob ID not found" << std::endl; - return; - } - - Mob* mob = Mobs[pktdata[i]]; - std::pair damage; - - damage.first = pkt->iTargetCnt > 1 ? bullet->groupDamage : bullet->pointDamage; - - int difficulty = (int)mob->data["m_iNpcLevel"]; - damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, NanoManager::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty); - - damage.first = hitMob(sock, mob, damage.first); - - respdata[i].iID = mob->appearanceData.iNPC_ID; - respdata[i].iDamage = damage.first; - respdata[i].iHP = mob->appearanceData.iHP; - respdata[i].iHitFlag = damage.second; - } - - resp->iPC_ID = plr->iID; - resp->iBulletID = pkt->iBulletID; - resp->Bullet.iID = bullet->bulletType; - sock->sendPacket((void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen); - PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen); - - Bullets[plr->iID].erase(resp->iBulletID); -} - -void MobManager::followToCombat(Mob *mob) { - if (Mobs.find(mob->groupLeader) != Mobs.end()) { - Mob* leadMob = Mobs[mob->groupLeader]; - for (int i = 0; i < 4; i++) { - if (leadMob->groupMember[i] == 0) - break; - - if (Mobs.find(leadMob->groupMember[i]) == Mobs.end()) { - std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; - continue; - } - Mob* followerMob = Mobs[leadMob->groupMember[i]]; - - if (followerMob->state != MobState::ROAMING) // only roaming mobs should transition to combat - continue; - - enterCombat(mob->target, followerMob); - } - - if (leadMob->state != MobState::ROAMING) - return; - - enterCombat(mob->target, leadMob); - } -} - -void MobManager::groupRetreat(Mob *mob) { - if (Mobs.find(mob->groupLeader) == Mobs.end()) - return; - - Mob* leadMob = Mobs[mob->groupLeader]; - for (int i = 0; i < 4; i++) { - if (leadMob->groupMember[i] == 0) - break; - - if (Mobs.find(leadMob->groupMember[i]) == Mobs.end()) { - std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; - continue; - } - Mob* followerMob = Mobs[leadMob->groupMember[i]]; - - followerMob->target = nullptr; - followerMob->state = MobState::RETREAT; - clearDebuff(followerMob); - } - - leadMob->target = nullptr; - leadMob->state = MobState::RETREAT; - clearDebuff(leadMob); -} - -void MobManager::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 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 targetData = {0, 0, 0, 0, 0}; - - // find the players within range of eruption - for (auto it = mob->viewableChunks->begin(); it != mob->viewableChunks->end(); it++) { - Chunk* chunk = *it; - for (CNSocket *s : chunk->players) { - Player *plr = PlayerManager::getPlayer(s); - - if (plr->HP <= 0) - continue; - - int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y); - if (distance < NanoManager::SkillTable[skillID].effectArea) { - targetData[0] += 1; - targetData[targetData[0]] = plr->iID; - if (targetData[0] > 3) // make sure not to have more than 4 - break; - } - } - } - - for (auto& pwr : MobPowers) - if (pwr.skillType == NanoManager::SkillTable[skillID].skillType) - pwr.handle(mob, targetData, skillID, NanoManager::SkillTable[skillID].durationTime[0], NanoManager::SkillTable[skillID].powerIntensity[0]); - mob->skillStyle = -3; // eruption cooldown - mob->nextAttack = currTime + 1000; - return; - } - - if (mob->skillStyle == -3) { // cooldown expires - mob->skillStyle = -1; - return; - } - - int random = rand() % 2000 * 1000; - 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 targetData = {1, plr->iID, 0, 0, 0}; - for (auto& pwr : MobPowers) - if (pwr.skillType == NanoManager::SkillTable[skillID].skillType) { - if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag)) - return; // prevent debuffing a player twice - pwr.handle(mob, targetData, skillID, NanoManager::SkillTable[skillID].durationTime[0], NanoManager::SkillTable[skillID].powerIntensity[0]); - } - 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); - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; - pkt.iSkillID = skillID; - pkt.iValue1 = plr->x; - pkt.iValue2 = plr->y; - pkt.iValue3 = plr->z; - mob->skillStyle = NanoManager::nanoStyle(plr->activeNano) - 1; - if (mob->skillStyle == -1) - mob->skillStyle = 2; - if (mob->skillStyle == -2) - mob->skillStyle = rand() % 3; - 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); - pkt.iNPC_ID = mob->appearanceData.iNPC_ID; - 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; -} - -void MobManager::dealCorruption(Mob *mob, std::vector 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)); - - resp->iNPC_ID = mob->appearanceData.iNPC_ID; - 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; - - int style2 = NanoManager::nanoStyle(plr->activeNano); - if (style2 == -1) { // no nano - respdata[i].iHitFlag = 8; - respdata[i].iDamage = NanoManager::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; - } else if (style == style2) { - respdata[i].iHitFlag = 8; // tie - respdata[i].iDamage = 0; - respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina; - } else if (style - style2 == 1 || style2 - style == 2) { - respdata[i].iHitFlag = 4; // win - 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 - std::vector targetData2 = {1, mob->appearanceData.iNPC_ID, 0, 0, 0}; - for (auto& pwr : NanoManager::NanoPowers) - if (pwr.skillType == EST_DAMAGE) - pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200); - } else { - respdata[i].iHitFlag = 16; // lose - respdata[i].iDamage = NanoManager::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; - 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) { - mob->target = nullptr; - mob->state = MobState::RETREAT; - if (!aggroCheck(mob, getTime())) { - clearDebuff(mob); - if (mob->groupLeader != 0) - groupRetreat(mob); - } - } - } - - NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_NPC_SKILL_CORRUPTION_HIT, resplen); -} - -void MobManager::enterCombat(CNSocket *sock, Mob *mob) { - mob->target = sock; - mob->state = MobState::COMBAT; - mob->nextMovement = getTime(); - mob->nextAttack = 0; - - mob->roamX = mob->appearanceData.iX; - mob->roamY = mob->appearanceData.iY; - mob->roamZ = mob->appearanceData.iZ; - - int skillID = (int)mob->data["m_iPassiveBuff"]; // cast passive - std::vector targetData = {1, mob->appearanceData.iNPC_ID, 0, 0, 0}; - for (auto& pwr : MobPowers) - if (pwr.skillType == NanoManager::SkillTable[skillID].skillType) - pwr.handle(mob, targetData, skillID, NanoManager::SkillTable[skillID].durationTime[0], NanoManager::SkillTable[skillID].powerIntensity[0]); - - for (NPCEvent& event : NPCManager::NPCEvents) // trigger an ON_COMBAT - if (event.trigger == ON_COMBAT && event.npcType == mob->appearanceData.iNPCType) - event.handler(sock, mob); -} diff --git a/src/Combat.hpp b/src/Combat.hpp new file mode 100644 index 0000000..60c2871 --- /dev/null +++ b/src/Combat.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "CNProtocol.hpp" +#include "CNShared.hpp" +#include "CNShardServer.hpp" +#include "NPC.hpp" +#include "MobAI.hpp" + +#include "JSON.hpp" + +#include +#include +#include + +struct MobDropChance { + int dropChance; + std::vector cratesRatio; +}; + +struct MobDrop { + std::vector crateIDs; + int dropChanceType; + int taros; + int fm; + int boosts; +}; + +struct Bullet { + int pointDamage; + int groupDamage; + bool weaponBoost; + int bulletType; +}; + +namespace Combat { + extern std::map MobDropChances; + extern std::map MobDrops; + extern std::map> Bullets; + + void init(); + void playerTick(CNServer*, time_t); + + void pcAttackNpcs(CNSocket *sock, CNPacketData *data); + void combatBegin(CNSocket *sock, CNPacketData *data); + void combatEnd(CNSocket *sock, CNPacketData *data); + void dotDamageOnOff(CNSocket *sock, CNPacketData *data); + void dealGooDamage(CNSocket *sock, int amount); + + void npcAttackPc(Mob *mob, time_t currTime); + int hitMob(CNSocket *sock, Mob *mob, int damage); + void killMob(CNSocket *sock, Mob *mob); + void giveReward(CNSocket *sock, Mob *mob, int rolledBoosts, int rolledPotions, int rolledCrate, int rolledCrateType, int rolledEvent); + void getReward(sItemBase *reward, MobDrop *drop, MobDropChance *chance, int rolled); + void giveEventReward(CNSocket* sock, Player* player, int rolled); + + std::pair lerp(int, int, int, int, int); + std::pair getDamage(int, int, bool, bool, int, int, int); + + void pcAttackChars(CNSocket *sock, CNPacketData *data); + void grenadeFire(CNSocket* sock, CNPacketData* data); + void rocketFire(CNSocket* sock, CNPacketData* data); + void projectileHit(CNSocket* sock, CNPacketData* data); + /// returns bullet id + int8_t addBullet(Player* plr, bool isGrenade); +} diff --git a/src/CustomCommands.cpp b/src/CustomCommands.cpp index a1d269b..cb0376d 100644 --- a/src/CustomCommands.cpp +++ b/src/CustomCommands.cpp @@ -3,7 +3,7 @@ #include "PlayerManager.hpp" #include "TableData.hpp" #include "NPCManager.hpp" -#include "MobManager.hpp" +#include "MobAI.hpp" #include "ItemManager.hpp" #include "Database.hpp" #include "TransportManager.hpp" @@ -269,18 +269,18 @@ static void unsummonWCommand(std::string full, std::vector& args, C return; } - if (MobManager::Mobs.find(npc->appearanceData.iNPC_ID) != MobManager::Mobs.end()) { + if (MobAI::Mobs.find(npc->appearanceData.iNPC_ID) != MobAI::Mobs.end()) { int leadId = ((Mob*)npc)->groupLeader; if (leadId != 0) { - if (MobManager::Mobs.find(leadId) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(leadId) == MobAI::Mobs.end()) { std::cout << "[WARN] unsummonW: leader not found!" << std::endl; } - Mob* leadNpc = MobManager::Mobs[leadId]; + Mob* leadNpc = MobAI::Mobs[leadId]; for (int i = 0; i < 4; i++) { if (leadNpc->groupMember[i] == 0) break; - if (MobManager::Mobs.find(leadNpc->groupMember[i]) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(leadNpc->groupMember[i]) == MobAI::Mobs.end()) { std::cout << "[WARN] unsommonW: leader can't find a group member!" << std::endl; continue; } @@ -303,13 +303,13 @@ static void unsummonWCommand(std::string full, std::vector& args, C } static void toggleAiCommand(std::string full, std::vector& args, CNSocket* sock) { - MobManager::simulateMobs = !MobManager::simulateMobs; + MobAI::simulateMobs = !MobAI::simulateMobs; - if (MobManager::simulateMobs) + if (MobAI::simulateMobs) return; // return all mobs to their spawn points - for (auto& pair : MobManager::Mobs) { + for (auto& pair : MobAI::Mobs) { pair.second->state = MobState::RETREAT; pair.second->target = nullptr; pair.second->nextMovement = getTime(); @@ -596,7 +596,7 @@ static void summonGroupCommand(std::string full, std::vector& args, BaseNPC *npc = NPCManager::summonNPC(x, y, z, plr->instanceID, type, wCommand); if (team == 2 && i > 0) { leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID; - Mob* mob = MobManager::Mobs[npc->appearanceData.iNPC_ID]; + Mob* mob = MobAI::Mobs[npc->appearanceData.iNPC_ID]; mob->groupLeader = leadNpc->appearanceData.iNPC_ID; mob->offsetX = x - plr->x; mob->offsetY = y - plr->y; @@ -611,7 +611,7 @@ static void summonGroupCommand(std::string full, std::vector& args, if (team == 2 && i > 0) { leadNpc->groupMember[i-1] = npc->appearanceData.iNPC_ID; - Mob* mob = MobManager::Mobs[npc->appearanceData.iNPC_ID]; + Mob* mob = MobAI::Mobs[npc->appearanceData.iNPC_ID]; mob->groupLeader = leadNpc->appearanceData.iNPC_ID; mob->offsetX = x - plr->x; mob->offsetY = y - plr->y; @@ -626,7 +626,7 @@ static void summonGroupCommand(std::string full, std::vector& args, if (i == 0 && team == 2) { type = type2; - leadNpc = MobManager::Mobs[npc->appearanceData.iNPC_ID]; + leadNpc = MobAI::Mobs[npc->appearanceData.iNPC_ID]; leadNpc->groupLeader = leadNpc->appearanceData.iNPC_ID; } } diff --git a/src/MobAI.cpp b/src/MobAI.cpp new file mode 100644 index 0000000..f7f63d7 --- /dev/null +++ b/src/MobAI.cpp @@ -0,0 +1,805 @@ +#include "MobAI.hpp" +#include "Player.hpp" +#include "RacingManager.hpp" +#include "TransportManager.hpp" +#include "NanoManager.hpp" +#include "Combat.hpp" +#include "Abilities.hpp" + +#include +#include + +std::map MobAI::Mobs; +static std::queue RemovalQueue; + +bool MobAI::simulateMobs; + +static void roamingStep(Mob *mob, time_t currTime); + +/* + * Dynamic lerp; distinct from TransportManager::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. + */ +static std::pair lerp(int x1, int y1, int x2, int y2, int speed) { + std::pair 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; + mob->appearanceData.iConditionBitFlag = 0; + mob->unbuffTimes.clear(); + + INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); + pkt1.eCT = 2; + pkt1.iID = mob->appearanceData.iNPC_ID; + pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag; + NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); +} + +void MobAI::followToCombat(Mob *mob) { + if (Mobs.find(mob->groupLeader) != Mobs.end()) { + Mob* leadMob = Mobs[mob->groupLeader]; + for (int i = 0; i < 4; i++) { + if (leadMob->groupMember[i] == 0) + break; + + if (Mobs.find(leadMob->groupMember[i]) == Mobs.end()) { + std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; + continue; + } + Mob* followerMob = Mobs[leadMob->groupMember[i]]; + + if (followerMob->state != MobState::ROAMING) // only roaming mobs should transition to combat + continue; + + enterCombat(mob->target, followerMob); + } + + if (leadMob->state != MobState::ROAMING) + return; + + enterCombat(mob->target, leadMob); + } +} + +void MobAI::groupRetreat(Mob *mob) { + if (Mobs.find(mob->groupLeader) == Mobs.end()) + return; + + Mob* leadMob = Mobs[mob->groupLeader]; + for (int i = 0; i < 4; i++) { + if (leadMob->groupMember[i] == 0) + break; + + if (Mobs.find(leadMob->groupMember[i]) == Mobs.end()) { + std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; + continue; + } + Mob* followerMob = Mobs[leadMob->groupMember[i]]; + + followerMob->target = nullptr; + followerMob->state = MobState::RETREAT; + clearDebuff(followerMob); + } + + leadMob->target = nullptr; + leadMob->state = MobState::RETREAT; + 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; + + for (auto it = mob->viewableChunks->begin(); it != mob->viewableChunks->end(); it++) { + Chunk* chunk = *it; + for (CNSocket *s : chunk->players) { + Player *plr = PlayerManager::getPlayer(s); + + if (plr->HP <= 0) + continue; + + int mobRange = mob->sightRange; + + if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH + || RacingManager::EPRaces.find(s) != RacingManager::EPRaces.end()) + 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; + + if (mob->state != MobState::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)) + mobRange = -1; + + // height is relevant for aggro distance because of platforming + int xyDistance = hypot(mob->appearanceData.iX - plr->x, mob->appearanceData.iY - plr->y); + int distance = hypot(xyDistance, (mob->appearanceData.iZ - plr->z) * 2); // difference in Z counts twice + + if (distance > mobRange || distance > closestDistance) + continue; + + // found a player + closest = s; + closestDistance = distance; + } + } + + if (closest != nullptr) { + // found closest player. engage. + enterCombat(closest, mob); + + if (mob->groupLeader != 0) + followToCombat(mob); + + return true; + } + + return false; +} + +static void dealCorruption(Mob *mob, std::vector 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)); + + resp->iNPC_ID = mob->appearanceData.iNPC_ID; + 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; + + int style2 = NanoManager::nanoStyle(plr->activeNano); + if (style2 == -1) { // no nano + respdata[i].iHitFlag = 8; + respdata[i].iDamage = NanoManager::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; + } else if (style == style2) { + respdata[i].iHitFlag = 8; // tie + respdata[i].iDamage = 0; + respdata[i].iNanoStamina = plr->Nanos[plr->activeNano].iStamina; + } else if (style - style2 == 1 || style2 - style == 2) { + respdata[i].iHitFlag = 4; // win + 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 + std::vector targetData2 = {1, mob->appearanceData.iNPC_ID, 0, 0, 0}; + for (auto& pwr : NanoManager::NanoPowers) + if (pwr.skillType == EST_DAMAGE) + pwr.handle(sock, targetData2, plr->activeNano, skillID, 0, 200); + } else { + respdata[i].iHitFlag = 16; // lose + respdata[i].iDamage = NanoManager::SkillTable[skillID].powerIntensity[0] * PC_MAXHEALTH((int)mob->data["m_iNpcLevel"]) / 1500; + 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) { + mob->target = nullptr; + mob->state = MobState::RETREAT; + if (!MobAI::aggroCheck(mob, getTime())) { + MobAI::clearDebuff(mob); + if (mob->groupLeader != 0) + MobAI::groupRetreat(mob); + } + } + } + + 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 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 targetData = {0, 0, 0, 0, 0}; + + // find the players within range of eruption + for (auto it = mob->viewableChunks->begin(); it != mob->viewableChunks->end(); it++) { + Chunk* chunk = *it; + for (CNSocket *s : chunk->players) { + Player *plr = PlayerManager::getPlayer(s); + + if (plr->HP <= 0) + continue; + + int distance = hypot(mob->hitX - plr->x, mob->hitY - plr->y); + if (distance < NanoManager::SkillTable[skillID].effectArea) { + targetData[0] += 1; + targetData[targetData[0]] = plr->iID; + if (targetData[0] > 3) // make sure not to have more than 4 + break; + } + } + } + + for (auto& pwr : Combat::MobPowers) + if (pwr.skillType == NanoManager::SkillTable[skillID].skillType) + pwr.handle(mob, targetData, skillID, NanoManager::SkillTable[skillID].durationTime[0], NanoManager::SkillTable[skillID].powerIntensity[0]); + mob->skillStyle = -3; // eruption cooldown + mob->nextAttack = currTime + 1000; + return; + } + + if (mob->skillStyle == -3) { // cooldown expires + mob->skillStyle = -1; + return; + } + + int random = rand() % 2000 * 1000; + 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 targetData = {1, plr->iID, 0, 0, 0}; + for (auto& pwr : Combat::MobPowers) + if (pwr.skillType == NanoManager::SkillTable[skillID].skillType) { + if (pwr.bitFlag != 0 && (plr->iConditionBitFlag & pwr.bitFlag)) + return; // prevent debuffing a player twice + pwr.handle(mob, targetData, skillID, NanoManager::SkillTable[skillID].durationTime[0], NanoManager::SkillTable[skillID].powerIntensity[0]); + } + 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); + pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + pkt.iSkillID = skillID; + pkt.iValue1 = plr->x; + pkt.iValue2 = plr->y; + pkt.iValue3 = plr->z; + mob->skillStyle = NanoManager::nanoStyle(plr->activeNano) - 1; + if (mob->skillStyle == -1) + mob->skillStyle = 2; + if (mob->skillStyle == -2) + mob->skillStyle = rand() % 3; + 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); + pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + 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; +} + +void MobAI::enterCombat(CNSocket *sock, Mob *mob) { + mob->target = sock; + mob->state = MobState::COMBAT; + mob->nextMovement = getTime(); + mob->nextAttack = 0; + + mob->roamX = mob->appearanceData.iX; + mob->roamY = mob->appearanceData.iY; + mob->roamZ = mob->appearanceData.iZ; + + int skillID = (int)mob->data["m_iPassiveBuff"]; // cast passive + std::vector targetData = {1, mob->appearanceData.iNPC_ID, 0, 0, 0}; + for (auto& pwr : Combat::MobPowers) + if (pwr.skillType == NanoManager::SkillTable[skillID].skillType) + pwr.handle(mob, targetData, skillID, NanoManager::SkillTable[skillID].durationTime[0], NanoManager::SkillTable[skillID].powerIntensity[0]); + + for (NPCEvent& event : NPCManager::NPCEvents) // trigger an ON_COMBAT + if (event.trigger == ON_COMBAT && event.npcType == mob->appearanceData.iNPCType) + 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); + 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)); + + pkt->iID = mob->appearanceData.iNPC_ID; + pkt->eCT = 4; // mob + pkt->iTB_ID = ECSB_BOUNDINGBALL; + + drain->eCT = 4; + drain->iID = mob->appearanceData.iNPC_ID; + drain->iDamage = amount; + drain->iHP = mob->appearanceData.iHP -= amount; + + NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen); + + if (mob->appearanceData.iHP <= 0) + Combat::killMob(mob->target, mob); +} + +static void deadStep(Mob *mob, time_t currTime) { + // despawn the mob after a short delay + if (mob->killedTime != 0 && !mob->despawned && currTime - mob->killedTime > 2000) { + mob->despawned = true; + + INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt); + + pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + + NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT)); + + // if it was summoned, mark it for removal + if (mob->summoned) { + std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl; + RemovalQueue.push(mob->appearanceData.iNPC_ID); + return; + } + + // pre-set spawn coordinates if not marked for removal + mob->appearanceData.iX = mob->spawnX; + mob->appearanceData.iY = mob->spawnY; + mob->appearanceData.iZ = mob->spawnZ; + } + + // to guide their groupmates, group leaders still need to move despite being dead + if (mob->groupLeader == mob->appearanceData.iNPC_ID) + roamingStep(mob, currTime); + + if (mob->killedTime != 0 && currTime - mob->killedTime < mob->regenTime * 100) + return; + + std::cout << "respawning mob " << mob->appearanceData.iNPC_ID << " with HP = " << mob->maxHealth << std::endl; + + mob->appearanceData.iHP = mob->maxHealth; + mob->state = MobState::ROAMING; + + // if mob is a group leader/follower, spawn where the group is. + if (mob->groupLeader != 0) { + if (MobAI::Mobs.find(mob->groupLeader) != MobAI::Mobs.end()) { + Mob* leaderMob = MobAI::Mobs[mob->groupLeader]; + mob->appearanceData.iX = leaderMob->appearanceData.iX + mob->offsetX; + mob->appearanceData.iY = leaderMob->appearanceData.iY + mob->offsetY; + mob->appearanceData.iZ = leaderMob->appearanceData.iZ; + } else { + std::cout << "[WARN] deadStep: mob cannot find it's leader!" << std::endl; + } + } + + INITSTRUCT(sP_FE2CL_NPC_NEW, pkt); + + pkt.NPCAppearanceData = mob->appearanceData; + + // notify all nearby players + NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW)); +} + +static void combatStep(Mob *mob, time_t currTime) { + assert(mob->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 (!MobAI::aggroCheck(mob, currTime)) { + MobAI::clearDebuff(mob); + if (mob->groupLeader != 0) + MobAI::groupRetreat(mob); + } + return; + } + + Player *plr = PlayerManager::getPlayer(mob->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 (!MobAI::aggroCheck(mob, currTime)) { + MobAI::clearDebuff(mob); + if (mob->groupLeader != 0) + MobAI::groupRetreat(mob); + } + return; + } + + // drain + if (mob->skillStyle < 0 && (mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000) + && mob->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) { + drainMobHP(mob, mob->maxHealth / 20); // lose 5% every second + mob->lastDrainTime = currTime; + } + + // if drain killed the mob, return early + if (mob->appearanceData.iHP <= 0) + return; + + // unbuffing + std::unordered_map::iterator it = mob->unbuffTimes.begin(); + while (it != mob->unbuffTimes.end()) { + + if (currTime >= it->second) { + mob->appearanceData.iConditionBitFlag &= ~it->first; + + INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1); + pkt1.eCT = 2; + pkt1.iID = mob->appearanceData.iNPC_ID; + pkt1.iConditionBitFlag = mob->appearanceData.iConditionBitFlag; + NPCManager::sendToViewable(mob, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT)); + + it = mob->unbuffTimes.erase(it); + } else { + it++; + } + } + + // skip attack if stunned or asleep + if (mob->appearanceData.iConditionBitFlag & (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. + return; + } + + int distance = hypot(plr->x - mob->appearanceData.iX, plr->y - mob->appearanceData.iY); + int mobRange = (int)mob->data["m_iAtkRange"] + (int)mob->data["m_iRadius"]; + + if (currTime >= mob->nextAttack) { + if (mob->skillStyle != -1 || distance <= mobRange || rand() % 20 == 0) // while not in attack range, 1 / 20 chance. + useAbilities(mob, currTime); + if (mob->target == nullptr) + return; + } + + int distanceToTravel = INT_MAX; + int speed = mob->data["m_iRunSpeed"]; + // 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) + return; + mob->nextMovement = currTime + 400; + if (currTime >= mob->nextAttack) + mob->nextAttack = 0; + + // halve movement speed if snared + if (mob->appearanceData.iConditionBitFlag & 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); + } + + distanceToTravel = std::min(distance-mobRange+1, speed*2/5); + auto targ = lerp(mob->appearanceData.iX, mob->appearanceData.iY, targetX, targetY, distanceToTravel); + if (distanceToTravel < speed*2/5 && currTime >= mob->nextAttack) + mob->nextAttack = 0; + + NPCManager::updateNPCPosition(mob->appearanceData.iNPC_ID, targ.first, targ.second, mob->appearanceData.iZ, mob->instanceID, mob->appearanceData.iAngle); + + INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); + + pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + pkt.iSpeed = speed; + pkt.iToX = mob->appearanceData.iX = targ.first; + pkt.iToY = mob->appearanceData.iY = 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)); + } + + /* attack logic + * 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. + */ + 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); + } + } + + // 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; + MobAI::clearDebuff(mob); + if (mob->groupLeader != 0) + MobAI::groupRetreat(mob); + } +} + +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() % (delay/2); +} + +static void roamingStep(Mob *mob, 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 (MobAI::aggroCheck(mob, currTime)) + return; + } + + // no random roaming if the mob already has a set path + if (mob->staticPath) + return; + + if (mob->groupLeader != 0 && mob->groupLeader != mob->appearanceData.iNPC_ID) // don't roam by yourself without group leader + return; + + /* + * mob->nextMovement is also updated whenever the path queue is traversed in + * TransportManager::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) + return; + MobAI::incNextMovement(mob, currTime); + + int xStart = mob->spawnX - mob->idleRange/2; + int yStart = mob->spawnY - mob->idleRange/2; + int speed = mob->data["m_iWalkSpeed"]; + + // some mobs don't move (and we mustn't divide/modulus by zero) + if (mob->idleRange == 0 || speed == 0) + return; + + int farX, farY, distance; + int minDistance = mob->idleRange / 2; + + // pick a random destination + farX = xStart + rand() % mob->idleRange; + farY = yStart + rand() % mob->idleRange; + + distance = std::abs(std::max(farX - mob->appearanceData.iX, farY - mob->appearanceData.iY)); + if (distance == 0) + distance += 1; // hack to avoid FPE + + // if it's too short a walk, go further in that direction + farX = mob->appearanceData.iX + (farX - mob->appearanceData.iX) * minDistance / distance; + farY = mob->appearanceData.iY + (farY - mob->appearanceData.iY) * 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); + + // halve movement speed if snared + if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED) + speed /= 2; + + std::queue queue; + WarpLocation from = { mob->appearanceData.iX, mob->appearanceData.iY, mob->appearanceData.iZ }; + WarpLocation to = { farX, farY, mob->appearanceData.iZ }; + + // add a route to the queue; to be processed in TransportManager::stepNPCPathing() + TransportManager::lerp(&queue, from, to, speed); + TransportManager::NPCQueues[mob->appearanceData.iNPC_ID] = queue; + + if (mob->groupLeader != 0 && mob->groupLeader == mob->appearanceData.iNPC_ID) { + // make followers follow this npc. + for (int i = 0; i < 4; i++) { + if (mob->groupMember[i] == 0) + break; + + if (MobAI::Mobs.find(mob->groupMember[i]) == MobAI::Mobs.end()) { + std::cout << "[WARN] roamingStep: leader can't find a group member!" << std::endl; + continue; + } + + std::queue queue2; + Mob* followerMob = MobAI::Mobs[mob->groupMember[i]]; + from = { followerMob->appearanceData.iX, followerMob->appearanceData.iY, followerMob->appearanceData.iZ }; + to = { farX + followerMob->offsetX, farY + followerMob->offsetY, followerMob->appearanceData.iZ }; + TransportManager::lerp(&queue2, from, to, speed); + TransportManager::NPCQueues[followerMob->appearanceData.iNPC_ID] = queue2; + } + } +} + +static void retreatStep(Mob *mob, time_t currTime) { + if (mob->nextMovement != 0 && currTime < mob->nextMovement) + return; + + mob->nextMovement = currTime + 400; + + // distance between spawn point and current location + int distance = hypot(mob->appearanceData.iX - mob->roamX, mob->appearanceData.iY - mob->roamY); + + //if (distance > mob->data["m_iIdleRange"]) { + if (distance > 10) { + INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt); + + auto targ = lerp(mob->appearanceData.iX, mob->appearanceData.iY, mob->roamX, mob->roamY, (int)mob->data["m_iRunSpeed"]*4/5); + + pkt.iNPC_ID = mob->appearanceData.iNPC_ID; + pkt.iSpeed = (int)mob->data["m_iRunSpeed"] * 2; + pkt.iToX = mob->appearanceData.iX = targ.first; + pkt.iToY = mob->appearanceData.iY = targ.second; + pkt.iToZ = mob->appearanceData.iZ = mob->spawnZ; + pkt.iMoveStyle = 1; + + // notify all nearby players + NPCManager::sendToViewable(mob, &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->appearanceData.iHP = mob->maxHealth; + mob->killedTime = 0; + mob->nextAttack = 0; + mob->appearanceData.iConditionBitFlag = 0; + + // cast a return home heal spell, this is the right way(tm) + std::vector targetData = {1, 0, 0, 0, 0}; + for (auto& pwr : Combat::MobPowers) + if (pwr.skillType == NanoManager::SkillTable[110].skillType) + pwr.handle(mob, targetData, 110, NanoManager::SkillTable[110].durationTime[0], NanoManager::SkillTable[110].powerIntensity[0]); + // clear outlying debuffs + MobAI::clearDebuff(mob); + } +} + +static void step(CNServer *serv, time_t currTime) { + for (auto& pair : MobAI::Mobs) { + if (pair.second->playersInView < 0) + std::cout << "[WARN] Weird playerview value " << pair.second->playersInView << std::endl; + + // skip mob movement and combat if disabled or not in view + if ((!MobAI::simulateMobs || pair.second->playersInView == 0) && pair.second->state != MobState::DEAD + && pair.second->state != MobState::RETREAT) + continue; + + switch (pair.second->state) { + case MobState::INACTIVE: + // no-op + break; + case MobState::ROAMING: + roamingStep(pair.second, currTime); + break; + case MobState::COMBAT: + combatStep(pair.second, currTime); + break; + case MobState::RETREAT: + retreatStep(pair.second, currTime); + break; + case MobState::DEAD: + deadStep(pair.second, currTime); + break; + } + } + + // deallocate all NPCs queued for removal + while (RemovalQueue.size() > 0) { + NPCManager::destroyNPC(RemovalQueue.front()); + RemovalQueue.pop(); + } +} + +void MobAI::init() { + REGISTER_SHARD_TIMER(step, 200); + + simulateMobs = settings::SIMULATEMOBS; +} diff --git a/src/MobManager.hpp b/src/MobAI.hpp similarity index 52% rename from src/MobManager.hpp rename to src/MobAI.hpp index f604408..ff52f69 100644 --- a/src/MobManager.hpp +++ b/src/MobAI.hpp @@ -1,15 +1,7 @@ #pragma once #include "CNProtocol.hpp" -#include "CNShared.hpp" -#include "CNShardServer.hpp" -#include "NPC.hpp" - -#include "JSON.hpp" - -#include -#include -#include +#include "NPCManager.hpp" enum class MobState { INACTIVE, @@ -102,74 +94,17 @@ struct Mob : public BaseNPC { } }; -struct MobDropChance { - int dropChance; - std::vector cratesRatio; -}; - -struct MobDrop { - std::vector crateIDs; - int dropChanceType; - int taros; - int fm; - int boosts; -}; - -struct Bullet { - int pointDamage; - int groupDamage; - bool weaponBoost; - int bulletType; -}; - -namespace MobManager { - extern std::map Mobs; - extern std::queue RemovalQueue; - extern std::map MobDropChances; - extern std::map MobDrops; - extern std::map> Bullets; +namespace MobAI { extern bool simulateMobs; + extern std::map Mobs; void init(); - void step(CNServer*, time_t); - void playerTick(CNServer*, time_t); - void deadStep(Mob*, time_t); - void combatStep(Mob*, time_t); - void retreatStep(Mob*, time_t); - void roamingStep(Mob*, time_t); - - void pcAttackNpcs(CNSocket *sock, CNPacketData *data); - void combatBegin(CNSocket *sock, CNPacketData *data); - void combatEnd(CNSocket *sock, CNPacketData *data); - void dotDamageOnOff(CNSocket *sock, CNPacketData *data); - void dealGooDamage(CNSocket *sock, int amount); - - void npcAttackPc(Mob *mob, time_t currTime); - int hitMob(CNSocket *sock, Mob *mob, int damage); - void killMob(CNSocket *sock, Mob *mob); - void giveReward(CNSocket *sock, Mob *mob, int rolledBoosts, int rolledPotions, int rolledCrate, int rolledCrateType, int rolledEvent); - void getReward(sItemBase *reward, MobDrop *drop, MobDropChance *chance, int rolled); - void giveEventReward(CNSocket* sock, Player* player, int rolled); - - std::pair lerp(int, int, int, int, int); - std::pair getDamage(int, int, bool, bool, int, int, int); - - void pcAttackChars(CNSocket *sock, CNPacketData *data); - void drainMobHP(Mob *mob, int amount); + // TODO: make this internal later void incNextMovement(Mob *mob, time_t currTime=0); bool aggroCheck(Mob *mob, time_t currTime); void clearDebuff(Mob *mob); - - void grenadeFire(CNSocket* sock, CNPacketData* data); - void rocketFire(CNSocket* sock, CNPacketData* data); - void projectileHit(CNSocket* sock, CNPacketData* data); - /// returns bullet id - int8_t addBullet(Player* plr, bool isGrenade); - void followToCombat(Mob *mob); void groupRetreat(Mob *mob); - void useAbilities(Mob *mob, time_t currTime); - void dealCorruption(Mob *mob, std::vector targetData, int skillID, int style); void enterCombat(CNSocket *sock, Mob *mob); } diff --git a/src/NPCManager.cpp b/src/NPCManager.cpp index 26d3ad7..bd3ccf5 100644 --- a/src/NPCManager.cpp +++ b/src/NPCManager.cpp @@ -1,7 +1,7 @@ #include "NPCManager.hpp" #include "ItemManager.hpp" #include "settings.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" #include "MissionManager.hpp" #include "ChunkManager.hpp" #include "NanoManager.hpp" @@ -79,8 +79,8 @@ void NPCManager::destroyNPC(int32_t id) { ChunkManager::removeNPCFromChunks(ChunkManager::getViewableChunks(entity->chunkPos), id); // remove from mob manager - if (MobManager::Mobs.find(id) != MobManager::Mobs.end()) - MobManager::Mobs.erase(id); + if (MobAI::Mobs.find(id) != MobAI::Mobs.end()) + MobAI::Mobs.erase(id); // remove from eggs if (Eggs.find(id) != Eggs.end()) @@ -538,7 +538,7 @@ BaseNPC *NPCManager::summonNPC(int x, int y, int z, uint64_t instance, int type, if (team == 2) { npc = new Mob(x, y, z + EXTRA_HEIGHT, inst, type, NPCData[type], id); - MobManager::Mobs[id] = (Mob*)npc; + MobAI::Mobs[id] = (Mob*)npc; // re-enable respawning, if desired ((Mob*)npc)->summoned = !respawn; diff --git a/src/NanoManager.cpp b/src/NanoManager.cpp index eabddcb..8c79f34 100644 --- a/src/NanoManager.cpp +++ b/src/NanoManager.cpp @@ -3,7 +3,7 @@ #include "NanoManager.hpp" #include "PlayerManager.hpp" #include "NPCManager.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" #include "MissionManager.hpp" #include "GroupManager.hpp" #include "Abilities.hpp" diff --git a/src/PlayerManager.cpp b/src/PlayerManager.cpp index ee7da44..2a71ce2 100644 --- a/src/PlayerManager.cpp +++ b/src/PlayerManager.cpp @@ -8,7 +8,7 @@ #include "ChatManager.hpp" #include "Database.hpp" #include "BuddyManager.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" #include "RacingManager.hpp" #include "BuiltinCommands.hpp" #include "Abilities.hpp" @@ -59,7 +59,7 @@ void PlayerManager::removePlayer(CNSocket* key) { GroupManager::groupKickPlayer(plr); // remove player's bullets - MobManager::Bullets.erase(plr->iID); + Combat::Bullets.erase(plr->iID); // remove player's ongoing race, if it exists RacingManager::EPRaces.erase(key); diff --git a/src/TableData.cpp b/src/TableData.cpp index 02d361e..1aa8242 100644 --- a/src/TableData.cpp +++ b/src/TableData.cpp @@ -4,7 +4,7 @@ #include "ItemManager.hpp" #include "settings.hpp" #include "MissionManager.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" #include "ChunkManager.hpp" #include "NanoManager.hpp" #include "RacingManager.hpp" @@ -284,7 +284,7 @@ void TableData::init() { Mob *tmp = new Mob(npc["iX"], npc["iY"], npc["iZ"], npc["iAngle"], instanceID, npc["iNPCType"], td, nextId); NPCManager::NPCs[nextId] = tmp; - MobManager::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; + MobAI::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; NPCManager::updateNPCPosition(nextId, npc["iX"], npc["iY"], npc["iZ"], instanceID, npc["iAngle"]); nextId++; @@ -310,7 +310,7 @@ void TableData::init() { Mob* tmp = new Mob(leader["iX"], leader["iY"], leader["iZ"], leader["iAngle"], instanceID, leader["iNPCType"], td, nextId); NPCManager::NPCs[nextId] = tmp; - MobManager::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; + MobAI::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; NPCManager::updateNPCPosition(nextId, leader["iX"], leader["iY"], leader["iZ"], instanceID, leader["iAngle"]); tmp->groupLeader = nextId; @@ -325,7 +325,7 @@ void TableData::init() { Mob* tmpFol = new Mob((int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], leader["iAngle"], instanceID, follower["iNPCType"], tdFol, nextId); NPCManager::NPCs[nextId] = tmpFol; - MobManager::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; + MobAI::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; NPCManager::updateNPCPosition(nextId, (int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], instanceID, leader["iAngle"]); tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"]; @@ -364,7 +364,7 @@ void TableData::init() { if (team == 2) { NPCManager::NPCs[nextId] = new Mob(npc["iX"], npc["iY"], npc["iZ"], npc["iAngle"], instanceID, npc["iNPCType"], NPCManager::NPCData[(int)npc["iNPCType"]], nextId); - MobManager::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; + MobAI::Mobs[nextId] = (Mob*)NPCManager::NPCs[nextId]; } else NPCManager::NPCs[nextId] = new BaseNPC(npc["iX"], npc["iY"], npc["iZ"], npc["iAngle"], instanceID, npc["iNPCType"], nextId); @@ -472,7 +472,7 @@ void TableData::loadPaths(int* nextId) { // mob paths pathDataNPC = pathData["mob"]; for (nlohmann::json::iterator npcPath = pathDataNPC.begin(); npcPath != pathDataNPC.end(); npcPath++) { - for (auto& pair : MobManager::Mobs) { + for (auto& pair : MobAI::Mobs) { if (pair.second->appearanceData.iNPCType == npcPath.value()["iNPCType"]) { std::cout << "[INFO] Using static path for mob " << pair.second->appearanceData.iNPCType << " with ID " << pair.first << std::endl; @@ -518,7 +518,7 @@ void TableData::loadDrops() { for (nlohmann::json::iterator _cratesRatio = dropChance["CratesRatio"].begin(); _cratesRatio != dropChance["CratesRatio"].end(); _cratesRatio++) { toAdd.cratesRatio.push_back((int)_cratesRatio.value()); } - MobManager::MobDropChances[(int)dropChance["Type"]] = toAdd; + Combat::MobDropChances[(int)dropChance["Type"]] = toAdd; } // MobDrops @@ -532,21 +532,21 @@ void TableData::loadDrops() { toAdd.dropChanceType = (int)drop["DropChance"]; // Check if DropChance exists - if (MobManager::MobDropChances.find(toAdd.dropChanceType) == MobManager::MobDropChances.end()) { + if (Combat::MobDropChances.find(toAdd.dropChanceType) == Combat::MobDropChances.end()) { throw TableException(" MobDropChance not found: " + std::to_string((toAdd.dropChanceType))); } // Check if number of crates is correct - if (!(MobManager::MobDropChances[(int)drop["DropChance"]].cratesRatio.size() == toAdd.crateIDs.size())) { + if (!(Combat::MobDropChances[(int)drop["DropChance"]].cratesRatio.size() == toAdd.crateIDs.size())) { throw TableException(" DropType " + std::to_string((int)drop["DropType"]) + " contains invalid number of crates"); } toAdd.taros = (int)drop["Taros"]; toAdd.fm = (int)drop["FM"]; toAdd.boosts = (int)drop["Boosts"]; - MobManager::MobDrops[(int)drop["DropType"]] = toAdd; + Combat::MobDrops[(int)drop["DropType"]] = toAdd; } - std::cout << "[INFO] Loaded " << MobManager::MobDrops.size() << " Mob Drop Types"<< std::endl; + std::cout << "[INFO] Loaded " << Combat::MobDrops.size() << " Mob Drop Types"<< std::endl; // Rarity Ratios nlohmann::json rarities = dropData["RarityRatios"]; @@ -830,7 +830,7 @@ void TableData::loadGruntwork(int32_t *nextId) { // re-enable respawning ((Mob*)npc)->summoned = false; - MobManager::Mobs[npc->appearanceData.iNPC_ID] = (Mob*)npc; + MobAI::Mobs[npc->appearanceData.iNPC_ID] = (Mob*)npc; } else { npc = new BaseNPC(mob["iX"], mob["iY"], mob["iZ"], mob["iAngle"], instanceID, mob["iNPCType"], id); } @@ -853,7 +853,7 @@ void TableData::loadGruntwork(int32_t *nextId) { ((Mob*)tmp)->summoned = false; NPCManager::NPCs[*nextId] = tmp; - MobManager::Mobs[*nextId] = (Mob*)NPCManager::NPCs[*nextId]; + MobAI::Mobs[*nextId] = (Mob*)NPCManager::NPCs[*nextId]; NPCManager::updateNPCPosition(*nextId, leader["iX"], leader["iY"], leader["iZ"], instanceID, leader["iAngle"]); tmp->groupLeader = *nextId; @@ -872,7 +872,7 @@ void TableData::loadGruntwork(int32_t *nextId) { ((Mob*)tmp)->summoned = false; NPCManager::NPCs[*nextId] = tmpFol; - MobManager::Mobs[*nextId] = (Mob*)NPCManager::NPCs[*nextId]; + MobAI::Mobs[*nextId] = (Mob*)NPCManager::NPCs[*nextId]; NPCManager::updateNPCPosition(*nextId, (int)leader["iX"] + (int)follower["iOffsetX"], (int)leader["iY"] + (int)follower["iOffsetY"], leader["iZ"], instanceID, leader["iAngle"]); tmpFol->offsetX = follower.find("iOffsetX") == follower.end() ? 0 : (int)follower["iOffsetX"]; @@ -1008,11 +1008,11 @@ void TableData::flush() { // add follower data to vector; go until OOB or until follower ID is 0 for (int i = 0; i < 4 && m->groupMember[i] > 0; i++) { - if (MobManager::Mobs.find(m->groupMember[i]) == MobManager::Mobs.end()) { + if (MobAI::Mobs.find(m->groupMember[i]) == MobAI::Mobs.end()) { std::cout << "[WARN] Follower with ID " << m->groupMember[i] << " not found; skipping\n"; continue; } - followers.push_back(MobManager::Mobs[m->groupMember[i]]); + followers.push_back(MobAI::Mobs[m->groupMember[i]]); } } else { diff --git a/src/TransportManager.cpp b/src/TransportManager.cpp index dc63869..031f95d 100644 --- a/src/TransportManager.cpp +++ b/src/TransportManager.cpp @@ -4,7 +4,8 @@ #include "NanoManager.hpp" #include "TransportManager.hpp" #include "TableData.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" +#include "MobAI.hpp" #include #include @@ -272,7 +273,7 @@ void TransportManager::stepNPCPathing() { } // skip if not simulating mobs - if (npc->npcClass == NPC_MOB && !MobManager::simulateMobs) { + if (npc->npcClass == NPC_MOB && !MobAI::simulateMobs) { it++; continue; } @@ -307,7 +308,7 @@ void TransportManager::stepNPCPathing() { NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE)); break; case NPC_MOB: - MobManager::incNextMovement((Mob*)npc); + MobAI::incNextMovement((Mob*)npc); /* fallthrough */ default: INITSTRUCT(sP_FE2CL_NPC_MOVE, move); diff --git a/src/main.cpp b/src/main.cpp index 4d525dc..34042f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,8 @@ #include "BuiltinCommands.hpp" #include "ChatManager.hpp" #include "CustomCommands.hpp" -#include "MobManager.hpp" +#include "Combat.hpp" +#include "MobAI.hpp" #include "ItemManager.hpp" #include "MissionManager.hpp" #include "NanoManager.hpp" @@ -101,7 +102,8 @@ int main() { BuiltinCommands::init(); ChatManager::init(); CustomCommands::init(); - MobManager::init(); + Combat::init(); + MobAI::init(); ItemManager::init(); MissionManager::init(); NanoManager::init();