2020-09-16 19:46:15 +00:00
|
|
|
#include "MobManager.hpp"
|
2020-08-27 20:16:52 +00:00
|
|
|
#include "PlayerManager.hpp"
|
2020-10-02 21:03:11 +00:00
|
|
|
#include "NanoManager.hpp"
|
2020-08-27 20:16:52 +00:00
|
|
|
#include "NPCManager.hpp"
|
2020-08-28 16:25:03 +00:00
|
|
|
#include "ItemManager.hpp"
|
2020-09-10 16:40:38 +00:00
|
|
|
#include "MissionManager.hpp"
|
2020-10-04 23:54:08 +00:00
|
|
|
#include "GroupManager.hpp"
|
2020-10-12 19:03:18 +00:00
|
|
|
#include "TransportManager.hpp"
|
2020-08-27 20:16:52 +00:00
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
#include <cmath>
|
2020-08-28 16:25:03 +00:00
|
|
|
#include <assert.h>
|
2020-08-27 20:16:52 +00:00
|
|
|
|
2020-09-16 23:43:48 +00:00
|
|
|
std::map<int32_t, Mob*> MobManager::Mobs;
|
2020-10-01 14:36:16 +00:00
|
|
|
std::queue<int32_t> MobManager::RemovalQueue;
|
2020-09-16 23:43:48 +00:00
|
|
|
|
2020-10-07 18:38:32 +00:00
|
|
|
bool MobManager::simulateMobs;
|
|
|
|
|
2020-09-16 19:46:15 +00:00
|
|
|
void MobManager::init() {
|
2020-09-22 19:31:08 +00:00
|
|
|
REGISTER_SHARD_TIMER(step, 200);
|
2020-09-26 22:16:15 +00:00
|
|
|
REGISTER_SHARD_TIMER(playerTick, 2000);
|
2020-09-22 19:31:08 +00:00
|
|
|
|
2020-08-27 20:16:52 +00:00
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_NPCs, pcAttackNpcs);
|
|
|
|
|
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_COMBAT_BEGIN, combatBegin);
|
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_COMBAT_END, combatEnd);
|
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_DOT_DAMAGE_ONOFF, dotDamageOnOff);
|
2020-09-22 15:30:49 +00:00
|
|
|
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_ATTACK_CHARs, pcAttackChars);
|
2020-10-07 18:38:32 +00:00
|
|
|
|
|
|
|
simulateMobs = settings::SIMULATEMOBS;
|
2020-08-27 20:16:52 +00:00
|
|
|
}
|
|
|
|
|
2020-09-16 19:46:15 +00:00
|
|
|
void MobManager::pcAttackNpcs(CNSocket *sock, CNPacketData *data) {
|
2020-08-27 20:16:52 +00:00
|
|
|
sP_CL2FE_REQ_PC_ATTACK_NPCs* pkt = (sP_CL2FE_REQ_PC_ATTACK_NPCs*)data->buf;
|
2020-08-28 20:01:49 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
2020-08-27 20:16:52 +00:00
|
|
|
|
2020-09-28 18:11:13 +00:00
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
|
|
|
|
2020-08-28 14:09:26 +00:00
|
|
|
// sanity check
|
|
|
|
if (!validInVarPacket(sizeof(sP_CL2FE_REQ_PC_ATTACK_NPCs), pkt->iNPCCnt, sizeof(int32_t), data->size)) {
|
|
|
|
std::cout << "[WARN] bad sP_CL2FE_REQ_PC_ATTACK_NPCs packet size\n";
|
2020-08-27 20:16:52 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-08-28 14:09:26 +00:00
|
|
|
|
2020-08-27 20:16:52 +00:00
|
|
|
int32_t *pktdata = (int32_t*)((uint8_t*)data->buf + sizeof(sP_CL2FE_REQ_PC_ATTACK_NPCs));
|
|
|
|
|
2020-08-28 14:09:26 +00:00
|
|
|
/*
|
|
|
|
* Due to the possibility of multiplication overflow (and regular buffer overflow),
|
2020-08-28 19:42:00 +00:00
|
|
|
* 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).
|
2020-08-28 14:09:26 +00:00
|
|
|
*/
|
|
|
|
if (!validOutVarPacket(sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC), pkt->iNPCCnt, sizeof(sAttackResult))) {
|
2020-08-28 16:25:03 +00:00
|
|
|
std::cout << "[WARN] bad sP_FE2CL_PC_ATTACK_NPCs_SUCC packet size\n";
|
2020-08-28 14:09:26 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-08-27 20:16:52 +00:00
|
|
|
|
|
|
|
// initialize response struct
|
|
|
|
size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) + pkt->iNPCCnt * sizeof(sAttackResult);
|
2020-09-09 19:08:31 +00:00
|
|
|
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
2020-08-28 14:09:26 +00:00
|
|
|
|
2020-08-27 20:16:52 +00:00
|
|
|
memset(respbuf, 0, resplen);
|
2020-08-28 14:09:26 +00:00
|
|
|
|
2020-08-27 20:16:52 +00:00
|
|
|
sP_FE2CL_PC_ATTACK_NPCs_SUCC *resp = (sP_FE2CL_PC_ATTACK_NPCs_SUCC*)respbuf;
|
|
|
|
sAttackResult *respdata = (sAttackResult*)(respbuf+sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC));
|
|
|
|
|
|
|
|
resp->iNPCCnt = pkt->iNPCCnt;
|
|
|
|
|
|
|
|
for (int i = 0; i < pkt->iNPCCnt; i++) {
|
2020-09-16 23:43:48 +00:00
|
|
|
if (Mobs.find(pktdata[i]) == Mobs.end()) {
|
2020-08-27 20:16:52 +00:00
|
|
|
// not sure how to best handle this
|
|
|
|
std::cout << "[WARN] pcAttackNpcs: mob ID not found" << std::endl;
|
|
|
|
return;
|
|
|
|
}
|
2020-09-16 23:43:48 +00:00
|
|
|
Mob *mob = Mobs[pktdata[i]];
|
2020-08-27 20:16:52 +00:00
|
|
|
|
2020-09-27 01:53:03 +00:00
|
|
|
std::pair<int,int> damage;
|
|
|
|
|
|
|
|
if (pkt->iNPCCnt > 1)
|
2020-10-09 00:01:35 +00:00
|
|
|
damage.first = plr->groupDamage;
|
2020-09-27 01:53:03 +00:00
|
|
|
else
|
2020-10-09 00:01:35 +00:00
|
|
|
damage.first = plr->pointDamage;
|
|
|
|
|
|
|
|
int difficulty = (int)mob->data["m_iNpcLevel"];
|
|
|
|
|
|
|
|
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW >= 11 + difficulty), NanoManager::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
|
|
|
|
|
|
|
|
if (plr->batteryW >= 11 + difficulty)
|
|
|
|
plr->batteryW -= 11 + difficulty;
|
2020-09-27 01:53:03 +00:00
|
|
|
|
|
|
|
damage.first = hitMob(sock, mob, damage.first);
|
2020-09-16 23:43:48 +00:00
|
|
|
|
|
|
|
respdata[i].iID = mob->appearanceData.iNPC_ID;
|
2020-09-27 01:53:03 +00:00
|
|
|
respdata[i].iDamage = damage.first;
|
2020-09-16 23:43:48 +00:00
|
|
|
respdata[i].iHP = mob->appearanceData.iHP;
|
2020-09-27 01:53:03 +00:00
|
|
|
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
|
2020-08-27 20:16:52 +00:00
|
|
|
}
|
|
|
|
|
2020-09-27 06:58:33 +00:00
|
|
|
resp->iBatteryW = plr->batteryW;
|
2020-08-27 20:16:52 +00:00
|
|
|
sock->sendPacket((void*)respbuf, P_FE2CL_PC_ATTACK_NPCs_SUCC, resplen);
|
2020-08-28 20:01:49 +00:00
|
|
|
|
2020-09-09 19:08:31 +00:00
|
|
|
// a bit of a hack: these are the same size, so we can reuse the response packet
|
2020-08-28 20:01:49 +00:00
|
|
|
assert(sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_NPCs));
|
|
|
|
sP_FE2CL_PC_ATTACK_NPCs *resp1 = (sP_FE2CL_PC_ATTACK_NPCs*)respbuf;
|
|
|
|
|
|
|
|
resp1->iPC_ID = plr->iID;
|
|
|
|
|
|
|
|
// send to other players
|
2020-09-17 22:45:43 +00:00
|
|
|
PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_ATTACK_NPCs, resplen);
|
2020-08-27 20:16:52 +00:00
|
|
|
}
|
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
void MobManager::npcAttackPc(Mob *mob, time_t currTime) {
|
2020-09-24 01:18:41 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(mob->target);
|
|
|
|
|
2020-09-28 18:11:13 +00:00
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
const size_t resplen = sizeof(sP_FE2CL_PC_ATTACK_NPCs_SUCC) + sizeof(sAttackResult);
|
|
|
|
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
|
|
|
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
|
|
|
memset(respbuf, 0, resplen);
|
|
|
|
|
|
|
|
sP_FE2CL_NPC_ATTACK_PCs *pkt = (sP_FE2CL_NPC_ATTACK_PCs*)respbuf;
|
|
|
|
sAttackResult *atk = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_NPC_ATTACK_PCs));
|
|
|
|
|
2020-10-09 00:01:35 +00:00
|
|
|
auto damage = getDamage(475 + (int)mob->data["m_iPower"], plr->defense, false, false, -1, -1, 1);
|
2020-09-27 01:53:03 +00:00
|
|
|
plr->HP -= damage.first;
|
2020-09-24 01:18:41 +00:00
|
|
|
|
|
|
|
pkt->iNPC_ID = mob->appearanceData.iNPC_ID;
|
|
|
|
pkt->iPCCnt = 1;
|
|
|
|
|
|
|
|
atk->iID = plr->iID;
|
2020-09-27 01:53:03 +00:00
|
|
|
atk->iDamage = damage.first;
|
2020-09-24 01:18:41 +00:00
|
|
|
atk->iHP = plr->HP;
|
2020-09-27 01:53:03 +00:00
|
|
|
atk->iHitFlag = damage.second;
|
2020-09-24 01:18:41 +00:00
|
|
|
|
|
|
|
mob->target->sendPacket((void*)respbuf, P_FE2CL_NPC_ATTACK_PCs, resplen);
|
2020-09-26 22:16:15 +00:00
|
|
|
PlayerManager::sendToViewable(mob->target, (void*)respbuf, P_FE2CL_NPC_ATTACK_PCs, resplen);
|
2020-09-24 01:18:41 +00:00
|
|
|
|
|
|
|
if (plr->HP <= 0) {
|
|
|
|
mob->target = nullptr;
|
|
|
|
mob->state = MobState::RETREAT;
|
2020-10-04 23:54:08 +00:00
|
|
|
aggroCheck(mob, currTime);
|
2020-09-24 01:18:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-16 19:46:15 +00:00
|
|
|
void MobManager::giveReward(CNSocket *sock) {
|
2020-08-27 20:16:52 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
2020-09-28 18:11:13 +00:00
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
|
|
|
|
2020-08-28 16:25:03 +00:00
|
|
|
const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward);
|
2020-09-08 01:01:47 +00:00
|
|
|
assert(resplen < CN_PACKET_BUFFER_SIZE - 8);
|
2020-08-28 16:25:03 +00:00
|
|
|
// we know it's only one trailing struct, so we can skip full validation
|
|
|
|
|
|
|
|
uint8_t respbuf[resplen]; // not a variable length array, don't worry
|
|
|
|
sP_FE2CL_REP_REWARD_ITEM *reward = (sP_FE2CL_REP_REWARD_ITEM *)respbuf;
|
|
|
|
sItemReward *item = (sItemReward *)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
|
|
|
|
|
|
|
// don't forget to zero the buffer!
|
|
|
|
memset(respbuf, 0, resplen);
|
|
|
|
|
2020-09-26 01:48:45 +00:00
|
|
|
// NOTE: these will need to be scaled according to the player/mob level difference
|
|
|
|
plr->money += (int)MissionManager::AvatarGrowth[plr->level]["m_iMobFM"]; // this one's innacurate, but close enough for now
|
|
|
|
MissionManager::updateFusionMatter(sock, MissionManager::AvatarGrowth[plr->level]["m_iMobFM"]);
|
2020-08-27 20:16:52 +00:00
|
|
|
|
2020-08-28 16:25:03 +00:00
|
|
|
// simple rewards
|
|
|
|
reward->m_iCandy = plr->money;
|
|
|
|
reward->m_iFusionMatter = plr->fusionmatter;
|
2020-09-27 06:58:33 +00:00
|
|
|
reward->m_iBatteryN = plr->batteryN;
|
|
|
|
reward->m_iBatteryW = plr->batteryW;
|
2020-08-28 16:25:03 +00:00
|
|
|
reward->iFatigue = 100; // prevents warning message
|
|
|
|
reward->iFatigue_Level = 1;
|
|
|
|
reward->iItemCnt = 1; // remember to update resplen if you change this
|
|
|
|
|
2020-09-26 14:45:32 +00:00
|
|
|
#if 0
|
2020-08-28 16:25:03 +00:00
|
|
|
int slot = ItemManager::findFreeSlot(plr);
|
|
|
|
if (slot == -1) {
|
2020-09-26 14:45:32 +00:00
|
|
|
#else
|
|
|
|
int slot = -1;
|
|
|
|
if (true) {
|
|
|
|
#endif
|
2020-08-28 16:25:03 +00:00
|
|
|
// no room for an item, but you still get FM and taros
|
2020-09-09 19:08:31 +00:00
|
|
|
reward->iItemCnt = 0;
|
2020-08-28 16:25:03 +00:00
|
|
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM));
|
|
|
|
} else {
|
|
|
|
// item reward
|
|
|
|
item->sItem.iType = 9;
|
|
|
|
item->sItem.iID = 1;
|
|
|
|
item->iSlotNum = slot;
|
|
|
|
item->eIL = 1; // Inventory Location. 1 means player inventory.
|
|
|
|
|
|
|
|
// update player
|
|
|
|
plr->Inven[slot] = item->sItem;
|
|
|
|
|
|
|
|
sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen);
|
|
|
|
}
|
2020-09-23 19:44:27 +00:00
|
|
|
|
2020-08-27 20:16:52 +00:00
|
|
|
}
|
2020-09-16 23:43:48 +00:00
|
|
|
|
2020-09-25 00:00:26 +00:00
|
|
|
int MobManager::hitMob(CNSocket *sock, Mob *mob, int damage) {
|
|
|
|
// cannot kill mobs multiple times; cannot harm retreating mobs
|
|
|
|
if (mob->state != MobState::ROAMING && mob->state != MobState::COMBAT) {
|
|
|
|
return 0; // no damage
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mob->state == MobState::ROAMING) {
|
|
|
|
assert(mob->target == nullptr);
|
|
|
|
mob->target = sock;
|
|
|
|
mob->state = MobState::COMBAT;
|
|
|
|
mob->nextMovement = getTime();
|
2020-09-26 22:16:15 +00:00
|
|
|
mob->nextAttack = 0;
|
2020-09-25 00:00:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
mob->appearanceData.iHP -= damage;
|
|
|
|
|
|
|
|
// wake up sleeping monster
|
|
|
|
// TODO: remove client-side bit somehow
|
|
|
|
mob->appearanceData.iConditionBitFlag &= ~CSB_BIT_MEZ;
|
|
|
|
|
|
|
|
if (mob->appearanceData.iHP <= 0)
|
|
|
|
killMob(mob->target, mob);
|
|
|
|
|
|
|
|
return damage;
|
|
|
|
}
|
|
|
|
|
2020-09-16 23:43:48 +00:00
|
|
|
void MobManager::killMob(CNSocket *sock, Mob *mob) {
|
|
|
|
mob->state = MobState::DEAD;
|
2020-09-24 01:18:41 +00:00
|
|
|
mob->target = nullptr;
|
2020-09-22 20:22:10 +00:00
|
|
|
mob->appearanceData.iConditionBitFlag = 0;
|
2020-09-16 23:43:48 +00:00
|
|
|
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
|
|
|
|
2020-09-22 15:30:49 +00:00
|
|
|
// check for the edge case where hitting the mob did not aggro it
|
|
|
|
if (sock != nullptr) {
|
2020-10-09 00:01:35 +00:00
|
|
|
Player* plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
|
|
|
|
giveReward(sock);
|
|
|
|
MissionManager::mobKilled(sock, mob->appearanceData.iNPCType);
|
|
|
|
} else {
|
|
|
|
plr = PlayerManager::getPlayerFromID(plr->iIDGroup);
|
|
|
|
|
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (int i = 0; i < plr->groupCnt; i++) {
|
|
|
|
CNSocket* sockTo = PlayerManager::getSockFromID(plr->groupIDs[i]);
|
|
|
|
giveReward(sockTo);
|
|
|
|
MissionManager::mobKilled(sockTo, mob->appearanceData.iNPCType);
|
|
|
|
}
|
|
|
|
}
|
2020-09-22 15:30:49 +00:00
|
|
|
}
|
2020-09-16 23:43:48 +00:00
|
|
|
|
2020-09-21 22:21:43 +00:00
|
|
|
mob->despawned = false;
|
2020-09-16 23:43:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MobManager::deadStep(Mob *mob, time_t currTime) {
|
2020-09-21 22:21:43 +00:00
|
|
|
// 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;
|
|
|
|
|
2020-09-25 00:00:26 +00:00
|
|
|
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
|
2020-09-24 22:36:25 +00:00
|
|
|
|
2020-10-01 14:36:16 +00:00
|
|
|
// if it was summoned, mark it for removal
|
2020-09-24 22:36:25 +00:00
|
|
|
if (mob->summoned) {
|
2020-10-01 14:36:16 +00:00
|
|
|
std::cout << "[INFO] Queueing killed summoned mob for removal" << std::endl;
|
|
|
|
RemovalQueue.push(mob->appearanceData.iNPC_ID);
|
2020-09-24 22:36:25 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-09-21 22:21:43 +00:00
|
|
|
}
|
|
|
|
|
2020-09-16 23:43:48 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
|
|
|
|
|
|
|
|
pkt.NPCAppearanceData = mob->appearanceData;
|
|
|
|
|
2020-09-21 21:13:13 +00:00
|
|
|
// notify all nearby players
|
2020-09-25 00:00:26 +00:00
|
|
|
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
|
2020-09-16 23:43:48 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
void MobManager::combatStep(Mob *mob, time_t currTime) {
|
|
|
|
assert(mob->target != nullptr);
|
|
|
|
|
|
|
|
// sanity check: did the target player lose connection?
|
|
|
|
if (PlayerManager::players.find(mob->target) == PlayerManager::players.end()) {
|
|
|
|
mob->target = nullptr;
|
|
|
|
mob->state = MobState::RETREAT;
|
2020-10-04 23:54:08 +00:00
|
|
|
aggroCheck(mob, currTime);
|
2020-09-24 01:18:41 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(mob->target);
|
|
|
|
|
2020-09-28 18:11:13 +00:00
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
|
|
|
|
2020-09-25 00:00:26 +00:00
|
|
|
// did something else kill the player in the mean time?
|
|
|
|
if (plr->HP <= 0) {
|
|
|
|
mob->target = nullptr;
|
|
|
|
mob->state = MobState::RETREAT;
|
2020-10-04 23:54:08 +00:00
|
|
|
aggroCheck(mob, currTime);
|
2020-09-24 01:18:41 +00:00
|
|
|
return;
|
2020-09-25 00:00:26 +00:00
|
|
|
}
|
2020-09-24 01:18:41 +00:00
|
|
|
|
|
|
|
int distance = hypot(plr->x - mob->appearanceData.iX, plr->y - mob->appearanceData.iY);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the mob is close enough to attack, do so. If not, get closer.
|
|
|
|
* No, I'm not 100% sure this is how it's supposed to work.
|
|
|
|
*/
|
|
|
|
if (distance <= (int)mob->data["m_iAtkRange"]) {
|
|
|
|
// attack logic
|
|
|
|
if (mob->nextAttack == 0) {
|
|
|
|
mob->nextAttack = currTime + (int)mob->data["m_iInitalTime"] * 100; // I *think* this is what this is
|
2020-10-04 23:54:08 +00:00
|
|
|
npcAttackPc(mob, currTime);
|
2020-09-24 01:18:41 +00:00
|
|
|
} else if (mob->nextAttack != 0 && currTime >= mob->nextAttack) {
|
|
|
|
mob->nextAttack = currTime + (int)mob->data["m_iDelayTime"] * 100;
|
2020-10-04 23:54:08 +00:00
|
|
|
npcAttackPc(mob, currTime);
|
2020-09-24 01:18:41 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// movement logic
|
|
|
|
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
|
|
|
|
return;
|
2020-09-27 02:28:06 +00:00
|
|
|
mob->nextMovement = currTime + 500;
|
2020-09-24 01:18:41 +00:00
|
|
|
|
2020-09-24 20:55:33 +00:00
|
|
|
int speed = mob->data["m_iRunSpeed"];
|
|
|
|
|
|
|
|
// halve movement speed if snared
|
|
|
|
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
|
|
|
|
speed /= 2;
|
|
|
|
|
|
|
|
auto targ = lerp(mob->appearanceData.iX, mob->appearanceData.iY, mob->target->plr->x, mob->target->plr->y, speed);
|
|
|
|
|
2020-09-25 01:58:20 +00:00
|
|
|
NPCManager::updateNPCPosition(mob->appearanceData.iNPC_ID, targ.first, targ.second, mob->appearanceData.iZ);
|
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
|
|
|
|
|
|
|
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
|
2020-09-24 20:55:33 +00:00
|
|
|
pkt.iSpeed = speed;
|
|
|
|
pkt.iToX = mob->appearanceData.iX = targ.first;
|
|
|
|
pkt.iToY = mob->appearanceData.iY = targ.second;
|
|
|
|
pkt.iToZ = mob->appearanceData.iZ;
|
2020-09-24 01:18:41 +00:00
|
|
|
|
|
|
|
// notify all nearby players
|
2020-09-25 00:00:26 +00:00
|
|
|
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
2020-09-24 01:18:41 +00:00
|
|
|
}
|
|
|
|
|
2020-09-26 22:16:15 +00:00
|
|
|
// retreat if the player leaves combat range
|
|
|
|
distance = hypot(plr->x - mob->spawnX, plr->y - mob->spawnY);
|
2020-09-24 20:55:33 +00:00
|
|
|
if (distance >= mob->data["m_iCombatRange"]) {
|
|
|
|
mob->target = nullptr;
|
|
|
|
mob->state = MobState::RETREAT;
|
|
|
|
}
|
2020-09-24 01:18:41 +00:00
|
|
|
}
|
|
|
|
|
2020-10-12 19:18:03 +00:00
|
|
|
void MobManager::incNextMovement(Mob *mob, time_t currTime) {
|
2020-10-12 19:03:18 +00:00
|
|
|
if (currTime == 0)
|
|
|
|
currTime = getTime();
|
|
|
|
|
|
|
|
int delay = (int)mob->data["m_iDelayTime"] * 1000;
|
|
|
|
mob->nextMovement = currTime + delay/2 + rand() % (delay/2);
|
|
|
|
}
|
|
|
|
|
2020-09-22 18:33:10 +00:00
|
|
|
void MobManager::roamingStep(Mob *mob, time_t currTime) {
|
2020-09-26 22:16:15 +00:00
|
|
|
/*
|
|
|
|
* We reuse nextAttack to avoid scanning for players all the time, but to still
|
|
|
|
* do so more often than if we waited for nextMovement (which is way too slow).
|
|
|
|
*/
|
2020-09-27 07:04:33 +00:00
|
|
|
if (mob->nextAttack == 0 || currTime >= mob->nextAttack) {
|
2020-10-04 23:54:08 +00:00
|
|
|
mob->nextAttack = currTime + 500;
|
|
|
|
if (aggroCheck(mob, currTime))
|
|
|
|
return;
|
2020-09-26 22:16:15 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 20:55:33 +00:00
|
|
|
// some mobs don't move (and we mustn't divide/modulus by zero)
|
|
|
|
if (mob->idleRange == 0)
|
|
|
|
return;
|
|
|
|
|
2020-09-22 18:33:10 +00:00
|
|
|
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
|
|
|
|
return;
|
2020-10-12 19:03:18 +00:00
|
|
|
incNextMovement(mob, currTime);
|
2020-09-22 18:33:10 +00:00
|
|
|
|
2020-10-12 19:03:18 +00:00
|
|
|
// only calculate a new route if we're not already on one
|
|
|
|
auto it = TransportManager::NPCQueues.find(mob->appearanceData.iNPC_ID);
|
|
|
|
if (it != TransportManager::NPCQueues.end() && it->second.empty())
|
|
|
|
return;
|
|
|
|
|
2020-09-22 18:33:10 +00:00
|
|
|
int xStart = mob->spawnX - mob->idleRange/2;
|
|
|
|
int yStart = mob->spawnY - mob->idleRange/2;
|
2020-09-24 20:55:33 +00:00
|
|
|
int speed = mob->data["m_iWalkSpeed"];
|
|
|
|
|
2020-10-12 19:03:18 +00:00
|
|
|
int farX, farY;
|
|
|
|
int distance; // for short walk detection
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We don't want the mob to just take one step and stop, so we make sure
|
|
|
|
* it has walked a half-decent distance.
|
|
|
|
*/
|
|
|
|
do {
|
|
|
|
farX = xStart + rand() % mob->idleRange;
|
|
|
|
farY = yStart + rand() % mob->idleRange;
|
|
|
|
|
|
|
|
distance = std::hypot(mob->appearanceData.iX - farX, mob->appearanceData.iY - farY);
|
|
|
|
} while (distance < 500);
|
|
|
|
|
2020-09-24 20:55:33 +00:00
|
|
|
// halve movement speed if snared
|
|
|
|
if (mob->appearanceData.iConditionBitFlag & CSB_BIT_DN_MOVE_SPEED)
|
|
|
|
speed /= 2;
|
|
|
|
|
2020-10-12 19:03:18 +00:00
|
|
|
std::queue<WarpLocation> queue;
|
|
|
|
WarpLocation from = { mob->appearanceData.iX, mob->appearanceData.iY, mob->appearanceData.iZ };
|
|
|
|
WarpLocation to = { farX, farY, mob->appearanceData.iZ };
|
2020-09-25 01:58:20 +00:00
|
|
|
|
2020-10-12 19:03:18 +00:00
|
|
|
// 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;
|
2020-09-22 18:33:10 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
void MobManager::retreatStep(Mob *mob, time_t currTime) {
|
|
|
|
// distance between spawn point and current location
|
2020-09-27 02:28:06 +00:00
|
|
|
if (mob->nextMovement != 0 && currTime < mob->nextMovement)
|
|
|
|
return;
|
|
|
|
|
|
|
|
mob->nextMovement = currTime + 500;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
int distance = hypot(mob->appearanceData.iX - mob->spawnX, mob->appearanceData.iY - mob->spawnY);
|
|
|
|
|
2020-10-08 18:20:12 +00:00
|
|
|
//if (distance > mob->data["m_iIdleRange"]) {
|
|
|
|
if (distance > 10) {
|
2020-09-24 01:18:41 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
|
|
|
|
2020-10-12 19:03:18 +00:00
|
|
|
auto targ = lerp(mob->appearanceData.iX, mob->appearanceData.iY, mob->spawnX, mob->spawnY, (int)mob->data["m_iRunSpeed"] * 3);
|
2020-09-24 20:55:33 +00:00
|
|
|
|
2020-09-24 01:18:41 +00:00
|
|
|
pkt.iNPC_ID = mob->appearanceData.iNPC_ID;
|
2020-10-12 19:03:18 +00:00
|
|
|
pkt.iSpeed = (int)mob->data["m_iRunSpeed"] * 3;
|
2020-09-24 20:55:33 +00:00
|
|
|
pkt.iToX = mob->appearanceData.iX = targ.first;
|
|
|
|
pkt.iToY = mob->appearanceData.iY = targ.second;
|
2020-09-24 01:18:41 +00:00
|
|
|
pkt.iToZ = mob->appearanceData.iZ;
|
|
|
|
|
|
|
|
// notify all nearby players
|
2020-09-25 00:00:26 +00:00
|
|
|
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
2020-09-24 01:18:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if we got there
|
2020-10-07 23:21:48 +00:00
|
|
|
//if (distance <= mob->data["m_iIdleRange"]) {
|
|
|
|
if (distance <= 10) { // retreat back to the spawn point
|
2020-09-24 01:18:41 +00:00
|
|
|
mob->state = MobState::ROAMING;
|
|
|
|
mob->appearanceData.iHP = mob->maxHealth;
|
|
|
|
mob->killedTime = 0;
|
|
|
|
mob->nextAttack = 0;
|
|
|
|
mob->appearanceData.iConditionBitFlag = 0;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-29 21:27:48 +00:00
|
|
|
resendMobHP(mob);
|
2020-09-24 01:18:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-22 19:31:08 +00:00
|
|
|
void MobManager::step(CNServer *serv, time_t currTime) {
|
2020-09-16 23:43:48 +00:00
|
|
|
for (auto& pair : Mobs) {
|
2020-09-22 18:33:10 +00:00
|
|
|
int x = pair.second->appearanceData.iX;
|
|
|
|
int y = pair.second->appearanceData.iY;
|
|
|
|
|
|
|
|
// skip chunks without players
|
2020-10-01 01:44:37 +00:00
|
|
|
if (!ChunkManager::inPopulatedChunks(x, y, pair.second->instanceID))
|
2020-09-22 18:33:10 +00:00
|
|
|
continue;
|
|
|
|
|
2020-09-22 18:53:44 +00:00
|
|
|
// skip mob movement and combat if disabled
|
2020-10-07 18:38:32 +00:00
|
|
|
if (!simulateMobs && pair.second->state != MobState::DEAD
|
|
|
|
&& pair.second->state != MobState::RETREAT)
|
2020-09-22 18:53:44 +00:00
|
|
|
continue;
|
|
|
|
|
2020-09-25 00:00:26 +00:00
|
|
|
// skip attack/move if stunned or asleep
|
|
|
|
if (pair.second->appearanceData.iConditionBitFlag & (CSB_BIT_STUN|CSB_BIT_MEZ)
|
|
|
|
&& (pair.second->state == MobState::ROAMING || pair.second->state == MobState::COMBAT))
|
|
|
|
continue;
|
|
|
|
|
2020-09-16 23:43:48 +00:00
|
|
|
switch (pair.second->state) {
|
2020-09-24 20:55:33 +00:00
|
|
|
case MobState::INACTIVE:
|
|
|
|
// no-op
|
|
|
|
break;
|
2020-09-22 18:33:10 +00:00
|
|
|
case MobState::ROAMING:
|
|
|
|
roamingStep(pair.second, currTime);
|
|
|
|
break;
|
2020-09-24 01:18:41 +00:00
|
|
|
case MobState::COMBAT:
|
|
|
|
combatStep(pair.second, currTime);
|
|
|
|
break;
|
|
|
|
case MobState::RETREAT:
|
|
|
|
retreatStep(pair.second, currTime);
|
|
|
|
break;
|
2020-09-16 23:43:48 +00:00
|
|
|
case MobState::DEAD:
|
|
|
|
deadStep(pair.second, currTime);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-10-01 14:36:16 +00:00
|
|
|
|
|
|
|
// deallocate all NPCs queued for removal
|
|
|
|
while (RemovalQueue.size() > 0) {
|
|
|
|
NPCManager::destroyNPC(RemovalQueue.front());
|
|
|
|
RemovalQueue.pop();
|
|
|
|
}
|
2020-09-16 23:43:48 +00:00
|
|
|
}
|
2020-09-24 20:55:33 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* 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<int,int> MobManager::lerp(int x1, int y1, int x2, int y2, int speed) {
|
2020-09-29 21:27:48 +00:00
|
|
|
std::pair<int,int> ret = {x1, y1};
|
2020-09-24 20:55:33 +00:00
|
|
|
|
2020-09-27 02:28:06 +00:00
|
|
|
speed /= 2;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-29 21:27:48 +00:00
|
|
|
if (speed == 0)
|
|
|
|
return ret;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-24 20:55:33 +00:00
|
|
|
int distance = hypot(x1 - x2, y1 - y2);
|
|
|
|
|
2020-09-27 02:28:06 +00:00
|
|
|
if (distance > speed) {
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-27 02:28:06 +00:00
|
|
|
int lerps = distance / speed;
|
|
|
|
|
|
|
|
// interpolate only the first point
|
2020-10-05 00:03:13 +00:00
|
|
|
float frac = 1.0f / lerps;
|
2020-09-27 02:28:06 +00:00
|
|
|
|
|
|
|
ret.first = (x1 + (x2 - x1) * frac);
|
|
|
|
ret.second = (y1 + (y2 - y1) * frac);
|
|
|
|
} else {
|
|
|
|
ret.first = x2;
|
|
|
|
ret.second = y2;
|
|
|
|
}
|
2020-09-24 20:55:33 +00:00
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2020-09-26 22:16:15 +00:00
|
|
|
|
|
|
|
void MobManager::combatBegin(CNSocket *sock, CNPacketData *data) {
|
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
2020-09-28 18:11:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr == nullptr) {
|
|
|
|
std::cout << "[WARN] combatBegin: null player!" << std::endl;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
plr->inCombat = true;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
// HACK: make sure the player has the right weapon out for combat
|
|
|
|
INITSTRUCT(sP_FE2CL_PC_EQUIP_CHANGE, resp);
|
|
|
|
|
|
|
|
resp.iPC_ID = plr->iID;
|
|
|
|
resp.iEquipSlotNum = 0;
|
|
|
|
resp.EquipSlotItem = plr->Equip[0];
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
PlayerManager::sendToViewable(sock, (void*)&resp, P_FE2CL_PC_EQUIP_CHANGE, sizeof(sP_FE2CL_PC_EQUIP_CHANGE));
|
2020-09-26 22:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MobManager::combatEnd(CNSocket *sock, CNPacketData *data) {
|
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
2020-09-28 18:11:13 +00:00
|
|
|
|
|
|
|
if (plr != nullptr)
|
|
|
|
plr->inCombat = false;
|
2020-09-26 22:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MobManager::dotDamageOnOff(CNSocket *sock, CNPacketData *data) {
|
|
|
|
sP_CL2FE_DOT_DAMAGE_ONOFF *pkt = (sP_CL2FE_DOT_DAMAGE_ONOFF*)data->buf;
|
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr == nullptr) {
|
|
|
|
std::cout << "[WARN] dotDamageOnOff: null player!" << std::endl;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((plr->iConditionBitFlag & CSB_BIT_INFECTION) != (bool)pkt->iFlag)
|
|
|
|
plr->iConditionBitFlag ^= CSB_BIT_INFECTION;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
INITSTRUCT(sP_FE2CL_PC_BUFF_UPDATE, pkt1);
|
|
|
|
|
|
|
|
pkt1.eCSTB = ECSB_INFECTION; //eCharStatusTimeBuffID
|
|
|
|
pkt1.eTBU = 1; //eTimeBuffUpdate
|
|
|
|
pkt1.eTBT = 0; //eTimeBuffType 1 means nano
|
|
|
|
pkt1.iConditionBitFlag = plr->iConditionBitFlag;
|
|
|
|
|
|
|
|
sock->sendPacket((void*)&pkt1, P_FE2CL_PC_BUFF_UPDATE, sizeof(sP_FE2CL_PC_BUFF_UPDATE));
|
2020-09-26 22:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void MobManager::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);
|
2020-09-26 23:00:31 +00:00
|
|
|
uint8_t respbuf[CN_PACKET_BUFFER_SIZE];
|
2020-10-04 23:54:08 +00:00
|
|
|
Player *plr = PlayerManager::getPlayer(sock);
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
2020-09-26 22:16:15 +00:00
|
|
|
|
|
|
|
memset(respbuf, 0, resplen);
|
|
|
|
|
|
|
|
sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK *pkt = (sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK*)respbuf;
|
|
|
|
sSkillResult_DotDamage *dmg = (sSkillResult_DotDamage*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr->iConditionBitFlag & CSB_BIT_PROTECT_INFECTION) {
|
|
|
|
amount = -2; // -2 is the magic number for "Protected" to appear as the damage number
|
|
|
|
dmg->bProtected = 1;
|
2020-09-28 18:11:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
// it's hypothetically possible to have the protection bit without a nano
|
|
|
|
if (plr->activeNano != -1)
|
|
|
|
plr->Nanos[plr->activeNano].iStamina -= 3;
|
|
|
|
} else {
|
|
|
|
plr->HP -= amount;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (plr->activeNano != -1) {
|
|
|
|
dmg->iStamina = plr->Nanos[plr->activeNano].iStamina;
|
|
|
|
|
|
|
|
if (plr->Nanos[plr->activeNano].iStamina <= 0) {
|
|
|
|
dmg->bNanoDeactive = 1;
|
|
|
|
plr->Nanos[plr->activeNano].iStamina = 0;
|
|
|
|
NanoManager::summonNano(PlayerManager::getSockFromID(plr->iID), -1);
|
|
|
|
}
|
|
|
|
}
|
2020-09-26 22:16:15 +00:00
|
|
|
|
|
|
|
pkt->iID = plr->iID;
|
|
|
|
pkt->eCT = 1; // player
|
|
|
|
pkt->iTB_ID = ECSB_INFECTION; // sSkillResult_DotDamage
|
|
|
|
|
|
|
|
dmg->eCT = 1;
|
|
|
|
dmg->iID = plr->iID;
|
|
|
|
dmg->iDamage = amount;
|
|
|
|
dmg->iHP = plr->HP;
|
2020-10-04 23:54:08 +00:00
|
|
|
dmg->iConditionBitFlag = plr->iConditionBitFlag;
|
2020-09-26 22:16:15 +00:00
|
|
|
|
|
|
|
sock->sendPacket((void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
|
|
|
PlayerManager::sendToViewable(sock, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
|
|
|
}
|
|
|
|
|
|
|
|
void MobManager::playerTick(CNServer *serv, time_t currTime) {
|
2020-09-27 17:58:37 +00:00
|
|
|
static time_t lastHealTime = 0;
|
|
|
|
|
2020-09-26 22:16:15 +00:00
|
|
|
for (auto& pair : PlayerManager::players) {
|
|
|
|
CNSocket *sock = pair.first;
|
|
|
|
Player *plr = pair.second.plr;
|
|
|
|
bool transmit = false;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
// group ticks
|
|
|
|
if (plr->groupCnt > 1)
|
|
|
|
GroupManager::groupTickInfo(plr);
|
2020-09-26 22:16:15 +00:00
|
|
|
|
2020-09-27 18:29:26 +00:00
|
|
|
// do not tick dead players
|
|
|
|
if (plr->HP <= 0)
|
|
|
|
continue;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-26 22:16:15 +00:00
|
|
|
// fm patch/lake damage
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr->iConditionBitFlag & CSB_BIT_INFECTION)
|
2020-09-29 21:27:48 +00:00
|
|
|
dealGooDamage(sock, PC_MAXHEALTH(plr->level) * 3 / 20);
|
2020-09-26 22:16:15 +00:00
|
|
|
|
|
|
|
// heal
|
2020-09-29 21:27:48 +00:00
|
|
|
if (currTime - lastHealTime >= 6000 && !plr->inCombat && plr->HP < PC_MAXHEALTH(plr->level)) {
|
|
|
|
plr->HP += PC_MAXHEALTH(plr->level) / 5;
|
2020-09-26 22:16:15 +00:00
|
|
|
if (plr->HP > PC_MAXHEALTH(plr->level))
|
|
|
|
plr->HP = PC_MAXHEALTH(plr->level);
|
|
|
|
transmit = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
if (plr->activeNano != 0 && plr->equippedNanos[i] == plr->activeNano) { // spend stamina
|
2020-09-29 21:27:48 +00:00
|
|
|
plr->Nanos[plr->activeNano].iStamina -= 1;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-29 21:27:48 +00:00
|
|
|
if (plr->passiveNanoOut)
|
2020-10-05 00:03:13 +00:00
|
|
|
plr->Nanos[plr->activeNano].iStamina -= 1;
|
2020-09-26 22:16:15 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr->Nanos[plr->activeNano].iStamina <= 0) {
|
|
|
|
plr->Nanos[plr->activeNano].iStamina = 0;
|
2020-10-02 21:03:11 +00:00
|
|
|
NanoManager::summonNano(PlayerManager::getSockFromID(plr->iID), -1);
|
2020-10-04 23:54:08 +00:00
|
|
|
}
|
2020-09-26 22:16:15 +00:00
|
|
|
|
|
|
|
transmit = true;
|
|
|
|
} else if (plr->Nanos[plr->equippedNanos[i]].iStamina < 150) { // regain stamina
|
|
|
|
sNano& nano = plr->Nanos[plr->equippedNanos[i]];
|
2020-09-29 21:27:48 +00:00
|
|
|
nano.iStamina += 1;
|
2020-09-26 22:16:15 +00:00
|
|
|
|
|
|
|
if (nano.iStamina > 150)
|
|
|
|
nano.iStamina = 150;
|
|
|
|
|
|
|
|
transmit = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (transmit) {
|
|
|
|
INITSTRUCT(sP_FE2CL_REP_PC_TICK, pkt);
|
|
|
|
|
|
|
|
pkt.iHP = plr->HP;
|
|
|
|
pkt.iBatteryN = plr->batteryN;
|
|
|
|
|
|
|
|
pkt.aNano[0] = plr->Nanos[plr->equippedNanos[0]];
|
|
|
|
pkt.aNano[1] = plr->Nanos[plr->equippedNanos[1]];
|
|
|
|
pkt.aNano[2] = plr->Nanos[plr->equippedNanos[2]];
|
|
|
|
|
|
|
|
sock->sendPacket((void*)&pkt, P_FE2CL_REP_PC_TICK, sizeof(sP_FE2CL_REP_PC_TICK));
|
|
|
|
}
|
|
|
|
}
|
2020-09-27 17:58:37 +00:00
|
|
|
|
|
|
|
// if this was a heal tick, update the counter outside of the loop
|
2020-09-29 21:27:48 +00:00
|
|
|
if (currTime - lastHealTime >= 6000)
|
2020-09-27 17:58:37 +00:00
|
|
|
lastHealTime = currTime;
|
2020-09-26 22:16:15 +00:00
|
|
|
}
|
2020-09-27 01:53:03 +00:00
|
|
|
|
2020-10-12 19:03:18 +00:00
|
|
|
std::pair<int,int> MobManager::getDamage(int attackPower, int defensePower, bool shouldCrit,
|
|
|
|
bool batteryBoost, int attackerStyle,
|
|
|
|
int defenderStyle, int difficulty) {
|
2020-10-09 00:01:35 +00:00
|
|
|
std::pair<int,int> ret = {0, 1};
|
|
|
|
if (attackPower + defensePower * 2 == 0)
|
|
|
|
return ret;
|
2020-09-27 01:53:03 +00:00
|
|
|
|
2020-10-09 00:01:35 +00:00
|
|
|
int damage = attackPower * attackPower / (attackPower + defensePower);
|
|
|
|
damage = std::max(std::max(29, attackPower / 7), damage - defensePower * (12 + difficulty) / 65);
|
|
|
|
damage = damage * (rand() % 40 + 80) / 100;
|
|
|
|
|
|
|
|
if (attackerStyle != -1 && defenderStyle != -1 && attackerStyle != defenderStyle) {
|
|
|
|
if (attackerStyle < defenderStyle || attackerStyle - defenderStyle == 2)
|
|
|
|
damage = damage * 5 / 4;
|
|
|
|
else
|
|
|
|
damage = damage * 4 / 5;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (batteryBoost)
|
|
|
|
damage = damage * 5 / 4;
|
|
|
|
|
|
|
|
ret.first = damage;
|
2020-09-27 01:53:03 +00:00
|
|
|
ret.second = 1;
|
|
|
|
|
|
|
|
if (shouldCrit && rand() % 20 == 0) {
|
2020-09-27 02:28:06 +00:00
|
|
|
ret.first *= 2; // critical hit
|
2020-09-27 01:53:03 +00:00
|
|
|
ret.second = 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
2020-09-27 18:29:26 +00:00
|
|
|
}
|
2020-09-22 15:30:49 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2020-09-28 18:11:13 +00:00
|
|
|
if (plr == nullptr)
|
|
|
|
return;
|
|
|
|
|
2020-09-22 15:30:49 +00:00
|
|
|
// 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.plr->iID == pktdata[i*2]) {
|
|
|
|
target = pair.second.plr;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (target == nullptr) {
|
|
|
|
// you shall not pass
|
|
|
|
std::cout << "[WARN] pcAttackChars: player ID not found" << std::endl;
|
|
|
|
return;
|
|
|
|
}
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-29 21:27:48 +00:00
|
|
|
std::pair<int,int> damage;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-09-29 21:27:48 +00:00
|
|
|
if (pkt->iTargetCnt > 1)
|
2020-10-09 00:01:35 +00:00
|
|
|
damage.first = plr->groupDamage;
|
2020-09-29 21:27:48 +00:00
|
|
|
else
|
2020-10-09 00:01:35 +00:00
|
|
|
damage.first = plr->pointDamage;
|
|
|
|
|
|
|
|
damage = getDamage(damage.first, target->defense, true, (plr->batteryW >= 12), -1, -1, 1);
|
|
|
|
|
|
|
|
if (plr->batteryW >= 12)
|
|
|
|
plr->batteryW -= 12;
|
2020-09-22 15:30:49 +00:00
|
|
|
|
2020-09-29 21:27:48 +00:00
|
|
|
target->HP -= damage.first;
|
2020-09-22 15:30:49 +00:00
|
|
|
|
|
|
|
respdata[i].eCT = pktdata[i*2+1];
|
|
|
|
respdata[i].iID = target->iID;
|
2020-09-29 21:27:48 +00:00
|
|
|
respdata[i].iDamage = damage.first;
|
2020-09-22 15:30:49 +00:00
|
|
|
respdata[i].iHP = target->HP;
|
2020-09-29 21:27:48 +00:00
|
|
|
respdata[i].iHitFlag = damage.second; // hitscan, not a rocket or a grenade
|
2020-09-22 15:30:49 +00:00
|
|
|
} 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<int,int> damage;
|
|
|
|
|
|
|
|
if (pkt->iTargetCnt > 1)
|
2020-10-09 00:01:35 +00:00
|
|
|
damage.first = plr->groupDamage;
|
2020-09-22 15:30:49 +00:00
|
|
|
else
|
2020-10-09 00:01:35 +00:00
|
|
|
damage.first = plr->pointDamage;
|
|
|
|
|
|
|
|
int difficulty = (int)mob->data["m_iNpcLevel"];
|
|
|
|
|
2020-10-12 19:03:18 +00:00
|
|
|
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, (plr->batteryW >= 11 + difficulty),
|
|
|
|
NanoManager::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
|
2020-10-09 00:01:35 +00:00
|
|
|
|
|
|
|
if (plr->batteryW >= 11 + difficulty)
|
|
|
|
plr->batteryW -= 11 + difficulty;
|
2020-09-22 15:30:49 +00:00
|
|
|
|
|
|
|
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
|
2020-10-09 00:01:35 +00:00
|
|
|
assert(sizeof(sP_FE2CL_PC_ATTACK_CHARs_SUCC) == sizeof(sP_FE2CL_PC_ATTACK_CHARs));
|
2020-09-22 15:30:49 +00:00
|
|
|
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);
|
|
|
|
}
|
2020-09-29 21:27:48 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
// HACK: we haven't found a better way to refresh a mob's client-side status
|
2020-09-29 21:27:48 +00:00
|
|
|
void MobManager::resendMobHP(Mob *mob) {
|
|
|
|
size_t resplen = sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK) + sizeof(sSkillResult_Heal_HP);
|
|
|
|
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_Heal_HP *heal = (sSkillResult_Heal_HP*)(respbuf + sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_TICK));
|
|
|
|
|
|
|
|
pkt->iID = mob->appearanceData.iNPC_ID;
|
|
|
|
pkt->eCT = 4; // mob
|
|
|
|
pkt->iTB_ID = ECSB_HEAL; // sSkillResult_Heal_HP
|
|
|
|
|
|
|
|
heal->eCT = 4;
|
|
|
|
heal->iID = mob->appearanceData.iNPC_ID;
|
|
|
|
heal->iHealHP = 0;
|
|
|
|
heal->iHP = mob->appearanceData.iHP;
|
|
|
|
|
|
|
|
NPCManager::sendToViewable(mob, (void*)&respbuf, P_FE2CL_CHAR_TIME_BUFF_TIME_TICK, resplen);
|
2020-10-01 14:36:16 +00:00
|
|
|
}
|
2020-10-04 23:54:08 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* 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) {
|
|
|
|
for (Chunk *chunk : mob->currentChunks) {
|
|
|
|
for (CNSocket *s : chunk->players) {
|
|
|
|
Player *plr = s->plr;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr->HP <= 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
int mobRange = mob->data["m_iSightRange"];
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr->iConditionBitFlag & CSB_BIT_UP_STEALTH)
|
|
|
|
mobRange /= 3;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (plr->iSpecialState & CN_SPECIAL_STATE_FLAG__INVISIBLE)
|
|
|
|
mobRange = -1;
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
// 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);
|
2020-10-05 00:03:13 +00:00
|
|
|
|
2020-10-04 23:54:08 +00:00
|
|
|
if (distance > mobRange)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// found player. engage.
|
|
|
|
mob->target = s;
|
|
|
|
mob->state = MobState::COMBAT;
|
|
|
|
mob->nextMovement = currTime;
|
|
|
|
mob->nextAttack = 0;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|