Moved Debuff handling to Combat + Bugfixes

* Majority of mob debuff handling is moved to combatStep().
* Drain now kills the mob and does 40% overall damage.
* Bumped up active nano debuff durations, debuffs like drain linger longer but damage less.
* Debuffs are cleared upon mob death and retreating.
* Patched out vehicle off success packet spam
* Boosts and potions now cost the right amount (100 taros) and give the right quantity (100).
* Damage was tweaked slightly. At higher levels you are more likely to fall prey to rand().
* Enemies now use run animations during combat and retreating.
This commit is contained in:
Jade 2020-10-28 21:05:01 +00:00
parent 2af33da4e8
commit d21f727e9d
6 changed files with 85 additions and 62 deletions

View File

@ -144,7 +144,8 @@ void MobManager::npcAttackPc(Mob *mob, time_t currTime) {
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
aggroCheck(mob, currTime);
if (!aggroCheck(mob, currTime))
clearDebuff(mob);
}
}
@ -351,6 +352,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) {
mob->state = MobState::DEAD;
mob->target = nullptr;
mob->appearanceData.iConditionBitFlag = 0;
mob->unbuffTimes.clear();
mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
// check for the edge case where hitting the mob did not aggro it
@ -449,7 +451,8 @@ void MobManager::combatStep(Mob *mob, time_t currTime) {
if (PlayerManager::players.find(mob->target) == PlayerManager::players.end()) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
aggroCheck(mob, currTime);
if (!aggroCheck(mob, currTime))
clearDebuff(mob);
return;
}
@ -462,10 +465,40 @@ void MobManager::combatStep(Mob *mob, time_t currTime) {
if (plr->HP <= 0) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
aggroCheck(mob, currTime);
if (!aggroCheck(mob, currTime))
clearDebuff(mob);
return;
}
// drain
if ((mob->lastDrainTime == 0 || currTime - mob->lastDrainTime >= 1000) && mob->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) {
drainMobHP(mob, mob->maxHealth / 15); // lose 6.67% every second
mob->lastDrainTime = currTime;
}
// unbuffing
std::unordered_map<int32_t, time_t>::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))
return;
int distance = hypot(plr->x - mob->appearanceData.iX, plr->y - mob->appearanceData.iY);
/*
@ -481,6 +514,7 @@ void MobManager::combatStep(Mob *mob, time_t currTime) {
pkt.iToX = mob->appearanceData.iX;
pkt.iToY = mob->appearanceData.iY;
pkt.iToZ = mob->target->plr->z;
pkt.iMoveStyle = 1;
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
mob->nextAttack = currTime + (int)mob->data["m_iInitalTime"] * 100; //I *think* this is what this is
@ -514,6 +548,7 @@ void MobManager::combatStep(Mob *mob, time_t currTime) {
pkt.iToX = mob->appearanceData.iX = targ.first;
pkt.iToY = mob->appearanceData.iY = targ.second;
pkt.iToZ = mob->target->plr->z;
pkt.iMoveStyle = 1;
// notify all nearby players
NPCManager::sendToViewable(mob, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
@ -525,6 +560,7 @@ void MobManager::combatStep(Mob *mob, time_t currTime) {
if (distance >= mob->data["m_iCombatRange"]) {
mob->target = nullptr;
mob->state = MobState::RETREAT;
clearDebuff(mob);
}
}
@ -615,6 +651,7 @@ void MobManager::retreatStep(Mob *mob, time_t currTime) {
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));
@ -631,12 +668,11 @@ void MobManager::retreatStep(Mob *mob, time_t currTime) {
// HACK: we haven't found a better way to refresh a mob's client-side status
drainMobHP(mob, 0);
clearDebuff(mob);
}
}
void MobManager::step(CNServer *serv, time_t currTime) {
static time_t lastDrainTime = 0;
for (auto& pair : Mobs) {
int x = pair.second->appearanceData.iX;
int y = pair.second->appearanceData.iY;
@ -645,40 +681,11 @@ void MobManager::step(CNServer *serv, time_t currTime) {
if (!ChunkManager::inPopulatedChunks(x, y, pair.second->instanceID))
continue;
// drain
if (currTime - lastDrainTime >= 600 && pair.second->appearanceData.iConditionBitFlag & CSB_BIT_BOUNDINGBALL) {
drainMobHP(pair.second, pair.second->maxHealth * 3 / 50); // lose 10% every second
}
// unbuffing
std::unordered_map<int32_t, time_t>::iterator it = pair.second->unbuffTimes.begin();
while (it != pair.second->unbuffTimes.end()) {
if (currTime >= it->second) {
pair.second->appearanceData.iConditionBitFlag &= ~it->first;
INITSTRUCT(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT, pkt1);
pkt1.eCT = 2;
pkt1.iID = pair.second->appearanceData.iNPC_ID;
pkt1.iConditionBitFlag = pair.second->appearanceData.iConditionBitFlag;
NPCManager::sendToViewable(pair.second, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
it = pair.second->unbuffTimes.erase(it);
} else {
it++;
}
}
// skip mob movement and combat if disabled
if (!simulateMobs && pair.second->state != MobState::DEAD
&& pair.second->state != MobState::RETREAT)
continue;
// 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;
switch (pair.second->state) {
case MobState::INACTIVE:
// no-op
@ -698,9 +705,6 @@ void MobManager::step(CNServer *serv, time_t currTime) {
}
}
if (currTime - lastDrainTime >= 600)
lastDrainTime = currTime;
// deallocate all NPCs queued for removal
while (RemovalQueue.size() > 0) {
NPCManager::destroyNPC(RemovalQueue.front());
@ -920,7 +924,7 @@ std::pair<int,int> MobManager::getDamage(int attackPower, int defensePower, bool
// base calculation
int damage = attackPower * attackPower / (attackPower + defensePower);
damage = std::max(10 + attackPower / 10, damage - defensePower * (4 + difficulty) / 40);
//damage = std::max(10 + attackPower / 10, damage - defensePower * (4 + difficulty) / 100);
damage = damage * (rand() % 40 + 80) / 100;
// Adaptium/Blastons/Cosmix
@ -1082,6 +1086,9 @@ void MobManager::drainMobHP(Mob *mob, int 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);
}
/*
@ -1137,3 +1144,14 @@ bool MobManager::aggroCheck(Mob *mob, time_t currTime) {
return false;
}
void MobManager::clearDebuff(Mob *mob) {
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));
}

View File

@ -46,7 +46,7 @@ struct Mob : public BaseNPC {
// combat
CNSocket *target = nullptr;
time_t nextAttack = 0;
time_t lastDrainTime = 0;
// drop
int dropType;
@ -142,4 +142,5 @@ namespace MobManager {
void drainMobHP(Mob *mob, int amount);
void incNextMovement(Mob *mob, time_t currTime=0);
bool aggroCheck(Mob *mob, time_t currTime);
void clearDebuff(Mob *mob);
}

View File

@ -406,16 +406,16 @@ void NPCManager::npcVendorBuyBattery(CNSocket* sock, CNPacketData* data) {
if (plr == nullptr)
return;
int cost = req->Item.iOpt * 10;
int cost = req->Item.iOpt * 100;
if ((req->Item.iID == 3 ? (plr->batteryW >= 9999) : (plr->batteryN >= 9999)) || plr->money < cost) { // sanity check
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, failResp);
failResp.iErrorCode = 0;
sock->sendPacket((void*)&failResp, P_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL, sizeof(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_FAIL));
}
plr->money -= cost;
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 10 : 0;
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 10 : 0;
cost = plr->batteryW + plr->batteryN;
plr->batteryW += req->Item.iID == 3 ? req->Item.iOpt * 100 : 0;
plr->batteryN += req->Item.iID == 4 ? req->Item.iOpt * 100 : 0;
// caps
if (plr->batteryW > 9999)
@ -423,6 +423,9 @@ void NPCManager::npcVendorBuyBattery(CNSocket* sock, CNPacketData* data) {
if (plr->batteryN > 9999)
plr->batteryN = 9999;
cost = plr->batteryW + plr->batteryN - cost;
plr->money -= cost;
INITSTRUCT(sP_FE2CL_REP_PC_VENDOR_BATTERY_BUY_SUCC, resp);
resp.iCandy = plr->money;

View File

@ -715,15 +715,15 @@ void activePower(CNSocket *sock, CNPacketData *data,
// active nano power dispatch table
std::vector<ActivePower> ActivePowers = {
ActivePower(StunPowers, activePower<sSkillResult_Damage_N_Debuff, doDebuff>, EST_STUN, CSB_BIT_STUN, 2250),
ActivePower(StunPowers, activePower<sSkillResult_Damage_N_Debuff, doDebuff>, EST_STUN, CSB_BIT_STUN, 4000),
ActivePower(HealPowers, activePower<sSkillResult_Heal_HP, doHeal>, EST_HEAL_HP, CSB_BIT_NONE, 35),
ActivePower(GroupHealPowers, activePower<sSkillResult_Heal_HP, doGroupHeal, GHEAL>,EST_HEAL_HP, CSB_BIT_NONE, 20),
// TODO: Recall
ActivePower(DrainPowers, activePower<sSkillResult_Buff, doBuff>, EST_BOUNDINGBALL, CSB_BIT_BOUNDINGBALL, 3000),
ActivePower(SnarePowers, activePower<sSkillResult_Damage_N_Debuff, doDebuff>, EST_SNARE, CSB_BIT_DN_MOVE_SPEED, 4500),
ActivePower(DrainPowers, activePower<sSkillResult_Buff, doBuff>, EST_BOUNDINGBALL, CSB_BIT_BOUNDINGBALL, 6000),
ActivePower(SnarePowers, activePower<sSkillResult_Damage_N_Debuff, doDebuff>, EST_SNARE, CSB_BIT_DN_MOVE_SPEED, 8000),
ActivePower(DamagePowers, activePower<sSkillResult_Damage, doDamage>, EST_DAMAGE, CSB_BIT_NONE, 12),
ActivePower(LeechPowers, activePower<sSkillResult_Heal_HP, doLeech, LEECH>, EST_BLOODSUCKING, CSB_BIT_NONE, 18),
ActivePower(SleepPowers, activePower<sSkillResult_Damage_N_Debuff, doDebuff>, EST_SLEEP, CSB_BIT_MEZ, 4500),
ActivePower(SleepPowers, activePower<sSkillResult_Damage_N_Debuff, doDebuff>, EST_SLEEP, CSB_BIT_MEZ, 8000),
};
}; // namespace

View File

@ -855,10 +855,10 @@ void PlayerManager::enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
sock->sendPacket((void*)&response, P_FE2CL_PC_VEHICLE_ON_SUCC, sizeof(sP_FE2CL_PC_VEHICLE_ON_SUCC));
// send to other players
plr.plr->iPCState = 8;
plr.plr->iPCState |= 8;
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
response2.iPC_ID = plr.plr->iID;
response2.iState = 8;
response2.iState = plr.plr->iPCState;
for (Chunk* chunk : players[sock].currentChunks) {
for (CNSocket* otherSock : chunk->players) {
@ -879,20 +879,21 @@ void PlayerManager::enterPlayerVehicle(CNSocket* sock, CNPacketData* data) {
}
void PlayerManager::exitPlayerVehicle(CNSocket* sock, CNPacketData* data) {
PlayerView plr = PlayerManager::players[sock];
if (plr.plr->iPCState & 8) {
INITSTRUCT(sP_FE2CL_PC_VEHICLE_OFF_SUCC, response);
sock->sendPacket((void*)&response, P_FE2CL_PC_VEHICLE_OFF_SUCC, sizeof(sP_FE2CL_PC_VEHICLE_OFF_SUCC));
PlayerView plr = PlayerManager::players[sock];
// send to other players
plr.plr->iPCState = 0;
plr.plr->iPCState &= ~8;
INITSTRUCT(sP_FE2CL_PC_STATE_CHANGE, response2);
response2.iPC_ID = plr.plr->iID;
response2.iState = 0;
response2.iState = plr.plr->iPCState;
sendToViewable(sock, (void*)&response2, P_FE2CL_PC_STATE_CHANGE, sizeof(sP_FE2CL_PC_STATE_CHANGE));
}
}
void PlayerManager::setSpecialSwitchPlayer(CNSocket* sock, CNPacketData* data) {
setSpecialState(sock, data);