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