mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2024-11-26 15:00:06 +00:00
(WIP) Move away from rigid states/transitions to allow custom behavior
This commit is contained in:
parent
3f1f78942e
commit
7183180be5
@ -101,62 +101,29 @@ int32_t CombatNPC::getID() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CombatNPC::step(time_t currTime) {
|
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(stateHandlers.find(state) != stateHandlers.end())
|
||||||
if ((!MobAI::simulateMobs || playersInView == 0) && state != AIState::DEAD
|
stateHandlers[state](this, currTime);
|
||||||
&& state != AIState::RETREAT)
|
else {
|
||||||
return;
|
std::cout << "[WARN] State " << (int)state << " has no handler; going inactive" << std::endl;
|
||||||
|
transition(AIState::INACTIVE, id);
|
||||||
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) {
|
void CombatNPC::transition(AIState newState, EntityRef src) {
|
||||||
|
|
||||||
state = newState;
|
state = newState;
|
||||||
switch (newState) {
|
if (transitionHandlers.find(newState) != transitionHandlers.end())
|
||||||
case AIState::INACTIVE:
|
transitionHandlers[newState](this, src);
|
||||||
onInactive();
|
else {
|
||||||
break;
|
std::cout << "[WARN] Transition to " << (int)state << " has no handler; going inactive" << std::endl;
|
||||||
case AIState::ROAMING:
|
transition(AIState::INACTIVE, id);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
/* TODO: fire any triggered events
|
||||||
|
for (NPCEvent& event : NPCManager::NPCEvents)
|
||||||
|
if (event.trigger == ON_KILLED && event.npcType == type)
|
||||||
|
event.handler(src, this);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
|
static std::pair<int,int> getDamage(int attackPower, int defensePower, bool shouldCrit,
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
enum class EntityKind : uint8_t {
|
enum EntityKind {
|
||||||
INVALID,
|
INVALID,
|
||||||
PLAYER,
|
PLAYER,
|
||||||
SIMPLE_NPC,
|
SIMPLE_NPC,
|
||||||
@ -92,7 +92,6 @@ public:
|
|||||||
virtual bool isAlive() = 0;
|
virtual bool isAlive() = 0;
|
||||||
virtual int getCurrentHP() = 0;
|
virtual int getCurrentHP() = 0;
|
||||||
virtual int32_t getID() = 0;
|
virtual int32_t getID() = 0;
|
||||||
|
|
||||||
virtual void step(time_t currTime) = 0;
|
virtual void step(time_t currTime) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -133,11 +132,17 @@ struct CombatNPC : public BaseNPC, public ICombatant {
|
|||||||
AIState state = AIState::INACTIVE;
|
AIState state = AIState::INACTIVE;
|
||||||
int playersInView = 0; // for optimizing away AI in empty chunks
|
int playersInView = 0; // for optimizing away AI in empty chunks
|
||||||
|
|
||||||
|
std::map<AIState, void (*)(CombatNPC*, time_t)> stateHandlers;
|
||||||
|
std::map<AIState, void (*)(CombatNPC*, EntityRef)> transitionHandlers;
|
||||||
|
|
||||||
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) {
|
||||||
spawnX = x;
|
spawnX = x;
|
||||||
spawnY = y;
|
spawnY = y;
|
||||||
spawnZ = z;
|
spawnZ = z;
|
||||||
|
|
||||||
|
stateHandlers[AIState::INACTIVE] = {};
|
||||||
|
transitionHandlers[AIState::INACTIVE] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool isExtant() override { return hp > 0; }
|
virtual bool isExtant() override { return hp > 0; }
|
||||||
@ -147,19 +152,9 @@ struct CombatNPC : public BaseNPC, public ICombatant {
|
|||||||
virtual bool isAlive() override;
|
virtual bool isAlive() override;
|
||||||
virtual int getCurrentHP() override;
|
virtual int getCurrentHP() override;
|
||||||
virtual int32_t getID() override;
|
virtual int32_t getID() override;
|
||||||
|
|
||||||
virtual void step(time_t currTime) 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 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
|
||||||
|
360
src/MobAI.cpp
360
src/MobAI.cpp
@ -415,48 +415,71 @@ static void drainMobHP(Mob *mob, int amount) {
|
|||||||
mob->transition(AIState::DEAD, mob->target);
|
mob->transition(AIState::DEAD, mob->target);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mob::deadStep(time_t currTime) {
|
void MobAI::incNextMovement(Mob* mob, time_t currTime) {
|
||||||
|
if (currTime == 0)
|
||||||
|
currTime = getTime();
|
||||||
|
|
||||||
|
int delay = (int)mob->data["m_iDelayTime"] * 1000;
|
||||||
|
mob->nextMovement = currTime + delay / 2 + Rand::rand(delay / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Mob::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;
|
||||||
|
|
||||||
|
// call superclass step
|
||||||
|
CombatNPC::step(currTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MobAI::deadStep(CombatNPC* npc, time_t currTime) {
|
||||||
|
Mob* self = (Mob*)npc;
|
||||||
|
|
||||||
// despawn the mob after a short delay
|
// despawn the mob after a short delay
|
||||||
if (killedTime != 0 && !despawned && currTime - killedTime > 2000) {
|
if (self->killedTime != 0 && !self->despawned && currTime - self->killedTime > 2000) {
|
||||||
despawned = true;
|
self->despawned = true;
|
||||||
|
|
||||||
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
|
INITSTRUCT(sP_FE2CL_NPC_EXIT, pkt);
|
||||||
|
|
||||||
pkt.iNPC_ID = id;
|
pkt.iNPC_ID = self->id;
|
||||||
|
|
||||||
NPCManager::sendToViewable(this, &pkt, P_FE2CL_NPC_EXIT, sizeof(sP_FE2CL_NPC_EXIT));
|
NPCManager::sendToViewable(self, &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 (summoned) {
|
if (self->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(id);
|
NPCManager::queueNPCRemoval(self->id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-set spawn coordinates if not marked for removal
|
// pre-set spawn coordinates if not marked for removal
|
||||||
x = spawnX;
|
self->x = self->spawnX;
|
||||||
y = spawnY;
|
self->y = self->spawnY;
|
||||||
z = spawnZ;
|
self->z = self->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 (groupLeader == id)
|
if (self->groupLeader == self->id)
|
||||||
roamingStep(currTime);
|
roamingStep(self, currTime);
|
||||||
|
|
||||||
if (killedTime != 0 && currTime - killedTime < regenTime * 100)
|
if (self->killedTime != 0 && currTime - self->killedTime < self->regenTime * 100)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
std::cout << "respawning mob " << id << " with HP = " << maxHealth << std::endl;
|
std::cout << "respawning mob " << self->id << " with HP = " << self->maxHealth << std::endl;
|
||||||
|
|
||||||
transition(AIState::ROAMING, id);
|
self->transition(AIState::ROAMING, self->id);
|
||||||
|
|
||||||
// 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 (groupLeader != 0) {
|
if (self->groupLeader != 0) {
|
||||||
if (NPCManager::NPCs.find(groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[groupLeader]->kind == EntityKind::MOB) {
|
if (NPCManager::NPCs.find(self->groupLeader) != NPCManager::NPCs.end() && NPCManager::NPCs[self->groupLeader]->kind == EntityKind::MOB) {
|
||||||
Mob* leaderMob = (Mob*)NPCManager::NPCs[groupLeader];
|
Mob* leaderMob = (Mob*)NPCManager::NPCs[self->groupLeader];
|
||||||
x = leaderMob->x + offsetX;
|
self->x = leaderMob->x + self->offsetX;
|
||||||
y = leaderMob->y + offsetY;
|
self->y = leaderMob->y + self->offsetY;
|
||||||
z = leaderMob->z;
|
self->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;
|
||||||
}
|
}
|
||||||
@ -464,162 +487,157 @@ void Mob::deadStep(time_t currTime) {
|
|||||||
|
|
||||||
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
|
INITSTRUCT(sP_FE2CL_NPC_NEW, pkt);
|
||||||
|
|
||||||
pkt.NPCAppearanceData = getAppearanceData();
|
pkt.NPCAppearanceData = self->getAppearanceData();
|
||||||
|
|
||||||
// notify all nearby players
|
// notify all nearby players
|
||||||
NPCManager::sendToViewable(this, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
|
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_NEW, sizeof(sP_FE2CL_NPC_NEW));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mob::combatStep(time_t currTime) {
|
void MobAI::combatStep(CombatNPC* npc, time_t currTime) {
|
||||||
assert(target != nullptr);
|
Mob* self = (Mob*)npc;
|
||||||
|
assert(self->target != nullptr);
|
||||||
|
|
||||||
// lose aggro if the player lost connection
|
// lose aggro if the player lost connection
|
||||||
if (PlayerManager::players.find(target) == PlayerManager::players.end()) {
|
if (PlayerManager::players.find(self->target) == PlayerManager::players.end()) {
|
||||||
if (!MobAI::aggroCheck(this, getTime()))
|
if (!MobAI::aggroCheck(self, getTime()))
|
||||||
transition(AIState::RETREAT, target);
|
self->transition(AIState::RETREAT, self->target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Player *plr = PlayerManager::getPlayer(target);
|
Player *plr = PlayerManager::getPlayer(self->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)) {
|
||||||
if (!MobAI::aggroCheck(this, getTime()))
|
if (!MobAI::aggroCheck(self, getTime()))
|
||||||
transition(AIState::RETREAT, target);
|
self->transition(AIState::RETREAT, self->target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// drain
|
// drain
|
||||||
if (skillStyle < 0 && (lastDrainTime == 0 || currTime - lastDrainTime >= 1000)
|
if (self->skillStyle < 0 && (self->lastDrainTime == 0 || currTime - self->lastDrainTime >= 1000)
|
||||||
&& cbf & CSB_BIT_BOUNDINGBALL) {
|
&& self->cbf & CSB_BIT_BOUNDINGBALL) {
|
||||||
drainMobHP(this, maxHealth / 20); // lose 5% every second
|
drainMobHP(self, self->maxHealth / 20); // lose 5% every second
|
||||||
lastDrainTime = currTime;
|
self->lastDrainTime = currTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if drain killed the mob, return early
|
// if drain killed the mob, return early
|
||||||
if (hp <= 0)
|
if (self->hp <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// unbuffing
|
// unbuffing
|
||||||
std::unordered_map<int32_t, time_t>::iterator it = unbuffTimes.begin();
|
std::unordered_map<int32_t, time_t>::iterator it = self->unbuffTimes.begin();
|
||||||
while (it != unbuffTimes.end()) {
|
while (it != self->unbuffTimes.end()) {
|
||||||
|
|
||||||
if (currTime >= it->second) {
|
if (currTime >= it->second) {
|
||||||
cbf &= ~it->first;
|
self->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 = id;
|
pkt1.iID = self->id;
|
||||||
pkt1.iConditionBitFlag = cbf;
|
pkt1.iConditionBitFlag = self->cbf;
|
||||||
NPCManager::sendToViewable(this, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
NPCManager::sendToViewable(self, &pkt1, P_FE2CL_CHAR_TIME_BUFF_TIME_OUT, sizeof(sP_FE2CL_CHAR_TIME_BUFF_TIME_OUT));
|
||||||
|
|
||||||
it = unbuffTimes.erase(it);
|
it = self->unbuffTimes.erase(it);
|
||||||
} else {
|
} else {
|
||||||
it++;
|
it++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip attack if stunned or asleep
|
// skip attack if stunned or asleep
|
||||||
if (cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
|
if (self->cbf & (CSB_BIT_STUN|CSB_BIT_MEZ)) {
|
||||||
skillStyle = -1; // in this case we also reset the any outlying abilities the mob might be winding up.
|
self->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 - x, plr->y - y);
|
int distance = hypot(plr->x - self->x, plr->y - self->y);
|
||||||
int mobRange = (int)data["m_iAtkRange"] + (int)data["m_iRadius"];
|
int mobRange = (int)self->data["m_iAtkRange"] + (int)self->data["m_iRadius"];
|
||||||
|
|
||||||
if (currTime >= nextAttack) {
|
if (currTime >= self->nextAttack) {
|
||||||
if (skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
|
if (self->skillStyle != -1 || distance <= mobRange || Rand::rand(20) == 0) // while not in attack range, 1 / 20 chance.
|
||||||
useAbilities(this, currTime);
|
useAbilities(self, currTime);
|
||||||
if (target == nullptr)
|
if (self->target == nullptr)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int distanceToTravel = INT_MAX;
|
int distanceToTravel = INT_MAX;
|
||||||
// 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 && skillStyle == -1) {
|
if (distance > mobRange && self->skillStyle == -1) {
|
||||||
if (nextMovement != 0 && currTime < nextMovement)
|
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
||||||
return;
|
return;
|
||||||
nextMovement = currTime + 400;
|
self->nextMovement = currTime + 400;
|
||||||
if (currTime >= nextAttack)
|
if (currTime >= self->nextAttack)
|
||||||
nextAttack = 0;
|
self->nextAttack = 0;
|
||||||
|
|
||||||
// halve movement speed if snared
|
// halve movement speed if snared
|
||||||
if (cbf & CSB_BIT_DN_MOVE_SPEED)
|
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
|
||||||
speed /= 2;
|
self->speed /= 2;
|
||||||
|
|
||||||
int targetX = plr->x;
|
int targetX = plr->x;
|
||||||
int targetY = plr->y;
|
int targetY = plr->y;
|
||||||
if (groupLeader != 0) {
|
if (self->groupLeader != 0) {
|
||||||
targetX += offsetX*distance/(idleRange + 1);
|
targetX += self->offsetX*distance/(self->idleRange + 1);
|
||||||
targetY += offsetY*distance/(idleRange + 1);
|
targetY += self->offsetY*distance/(self->idleRange + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
distanceToTravel = std::min(distance-mobRange+1, speed*2/5);
|
distanceToTravel = std::min(distance-mobRange+1, self->speed*2/5);
|
||||||
auto targ = lerp(x, y, targetX, targetY, distanceToTravel);
|
auto targ = lerp(self->x, self->y, targetX, targetY, distanceToTravel);
|
||||||
if (distanceToTravel < speed*2/5 && currTime >= nextAttack)
|
if (distanceToTravel < self->speed*2/5 && currTime >= self->nextAttack)
|
||||||
nextAttack = 0;
|
self->nextAttack = 0;
|
||||||
|
|
||||||
NPCManager::updateNPCPosition(id, targ.first, targ.second, z, instanceID, angle);
|
NPCManager::updateNPCPosition(self->id, targ.first, targ.second, self->z, self->instanceID, self->angle);
|
||||||
|
|
||||||
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
INITSTRUCT(sP_FE2CL_NPC_MOVE, pkt);
|
||||||
|
|
||||||
pkt.iNPC_ID = id;
|
pkt.iNPC_ID = self->id;
|
||||||
pkt.iSpeed = speed;
|
pkt.iSpeed = self->speed;
|
||||||
pkt.iToX = x = targ.first;
|
pkt.iToX = self->x = targ.first;
|
||||||
pkt.iToY = y = targ.second;
|
pkt.iToY = self->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(this, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
NPCManager::sendToViewable(self, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* attack logic
|
/* attack logic
|
||||||
* 2/5 represents 400 ms which is the time interval mobs use per movement logic step
|
* 2/5 represents 400 ms which is the time interval mobs use per movement logic step
|
||||||
* if the mob is one move interval away, we should just start attacking anyways.
|
* if the mob is one move interval away, we should just start attacking anyways.
|
||||||
*/
|
*/
|
||||||
if (distance <= mobRange || distanceToTravel < speed*2/5) {
|
if (distance <= mobRange || distanceToTravel < self->speed*2/5) {
|
||||||
if (nextAttack == 0 || currTime >= nextAttack) {
|
if (self->nextAttack == 0 || currTime >= self->nextAttack) {
|
||||||
nextAttack = currTime + (int)data["m_iDelayTime"] * 100;
|
self->nextAttack = currTime + (int)self->data["m_iDelayTime"] * 100;
|
||||||
Combat::npcAttackPc(this, currTime);
|
Combat::npcAttackPc(self, currTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// retreat if the player leaves combat range
|
// retreat if the player leaves combat range
|
||||||
int xyDistance = hypot(plr->x - roamX, plr->y - roamY);
|
int xyDistance = hypot(plr->x - self->roamX, plr->y - self->roamY);
|
||||||
distance = hypot(xyDistance, plr->z - roamZ);
|
distance = hypot(xyDistance, plr->z - self->roamZ);
|
||||||
if (distance >= data["m_iCombatRange"]) {
|
if (distance >= self->data["m_iCombatRange"]) {
|
||||||
transition(AIState::RETREAT, target);
|
self->transition(AIState::RETREAT, self->target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MobAI::incNextMovement(Mob *mob, time_t currTime) {
|
void MobAI::roamingStep(CombatNPC* npc, time_t currTime) {
|
||||||
if (currTime == 0)
|
Mob* self = (Mob*)npc;
|
||||||
currTime = getTime();
|
|
||||||
|
|
||||||
int delay = (int)mob->data["m_iDelayTime"] * 1000;
|
|
||||||
mob->nextMovement = currTime + delay/2 + Rand::rand(delay/2);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (state != AIState::DEAD && (nextAttack == 0 || currTime >= nextAttack)) {
|
if (self->state != AIState::DEAD && (self->nextAttack == 0 || currTime >= self->nextAttack)) {
|
||||||
nextAttack = currTime + 500;
|
self->nextAttack = currTime + 500;
|
||||||
if (aggroCheck(this, currTime))
|
if (aggroCheck(self, 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 (staticPath)
|
if (self->staticPath)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (groupLeader != 0 && groupLeader != id) // don't roam by yourself without group leader
|
if (self->groupLeader != 0 && self->groupLeader != self->id) // don't roam by yourself without group leader
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -627,151 +645,157 @@ void Mob::roamingStep(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 (nextMovement != 0 && currTime < nextMovement)
|
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
||||||
return;
|
return;
|
||||||
incNextMovement(this, currTime);
|
incNextMovement(self, currTime);
|
||||||
|
|
||||||
int xStart = spawnX - idleRange/2;
|
int xStart = self->spawnX - self->idleRange/2;
|
||||||
int yStart = spawnY - idleRange/2;
|
int yStart = self->spawnY - self->idleRange/2;
|
||||||
|
|
||||||
// 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 (idleRange == 0 || speed == 0)
|
if (self->idleRange == 0 || self->speed == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int farX, farY, distance;
|
int farX, farY, distance;
|
||||||
int minDistance = idleRange / 2;
|
int minDistance = self->idleRange / 2;
|
||||||
|
|
||||||
// pick a random destination
|
// pick a random destination
|
||||||
farX = xStart + Rand::rand(idleRange);
|
farX = xStart + Rand::rand(self->idleRange);
|
||||||
farY = yStart + Rand::rand(idleRange);
|
farY = yStart + Rand::rand(self->idleRange);
|
||||||
|
|
||||||
distance = std::abs(std::max(farX - x, farY - y));
|
distance = std::abs(std::max(farX - self->x, farY - self->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 = x + (farX - x) * minDistance / distance;
|
farX = self->x + (farX - self->x) * minDistance / distance;
|
||||||
farY = y + (farY - y) * minDistance / distance;
|
farY = self->y + (farY - self->y) * minDistance / distance;
|
||||||
|
|
||||||
// but don't got out of bounds
|
// but don't got out of bounds
|
||||||
farX = std::clamp(farX, xStart, xStart + idleRange);
|
farX = std::clamp(farX, xStart, xStart + self->idleRange);
|
||||||
farY = std::clamp(farY, yStart, yStart + idleRange);
|
farY = std::clamp(farY, yStart, yStart + self->idleRange);
|
||||||
|
|
||||||
// halve movement speed if snared
|
// halve movement speed if snared
|
||||||
if (cbf & CSB_BIT_DN_MOVE_SPEED)
|
if (self->cbf & CSB_BIT_DN_MOVE_SPEED)
|
||||||
speed /= 2;
|
self->speed /= 2;
|
||||||
|
|
||||||
std::queue<Vec3> queue;
|
std::queue<Vec3> queue;
|
||||||
Vec3 from = { x, y, z };
|
Vec3 from = { self->x, self->y, self->z };
|
||||||
Vec3 to = { farX, farY, z };
|
Vec3 to = { farX, farY, self->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, self->speed);
|
||||||
Transport::NPCQueues[id] = queue;
|
Transport::NPCQueues[self->id] = queue;
|
||||||
|
|
||||||
if (groupLeader != 0 && groupLeader == id) {
|
if (self->groupLeader != 0 && self->groupLeader == self->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 (groupMember[i] == 0)
|
if (self->groupMember[i] == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (NPCManager::NPCs.find(groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[groupMember[i]]->kind != EntityKind::MOB) {
|
if (NPCManager::NPCs.find(self->groupMember[i]) == NPCManager::NPCs.end() || NPCManager::NPCs[self->groupMember[i]]->kind != EntityKind::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[groupMember[i]];
|
Mob* followerMob = (Mob*)NPCManager::NPCs[self->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, self->speed);
|
||||||
Transport::NPCQueues[followerMob->id] = queue2;
|
Transport::NPCQueues[followerMob->id] = queue2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mob::retreatStep(time_t currTime) {
|
void MobAI::retreatStep(CombatNPC* npc, time_t currTime) {
|
||||||
if (nextMovement != 0 && currTime < nextMovement)
|
Mob* self = (Mob*)npc;
|
||||||
|
|
||||||
|
if (self->nextMovement != 0 && currTime < self->nextMovement)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
nextMovement = currTime + 400;
|
self->nextMovement = currTime + 400;
|
||||||
|
|
||||||
// distance between spawn point and current location
|
// distance between spawn point and current location
|
||||||
int distance = hypot(x - roamX, y - roamY);
|
int distance = hypot(self->x - self->roamX, self->y - self->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(x, y, roamX, roamY, (int)speed*4/5);
|
auto targ = lerp(self->x, self->y, self->roamX, self->roamY, (int)self->speed*4/5);
|
||||||
|
|
||||||
pkt.iNPC_ID = id;
|
pkt.iNPC_ID = self->id;
|
||||||
pkt.iSpeed = (int)speed * 2;
|
pkt.iSpeed = (int)self->speed * 2;
|
||||||
pkt.iToX = x = targ.first;
|
pkt.iToX = self->x = targ.first;
|
||||||
pkt.iToY = y = targ.second;
|
pkt.iToY = self->y = targ.second;
|
||||||
pkt.iToZ = z = spawnZ;
|
pkt.iToZ = self->z = self->spawnZ;
|
||||||
pkt.iMoveStyle = 1;
|
pkt.iMoveStyle = 1;
|
||||||
|
|
||||||
// notify all nearby players
|
// notify all nearby players
|
||||||
NPCManager::sendToViewable(this, &pkt, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
|
NPCManager::sendToViewable(self, &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
|
||||||
transition(AIState::ROAMING, id);
|
self->transition(AIState::ROAMING, self->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mob::onInactive() {
|
void MobAI::onRoamStart(CombatNPC* npc, EntityRef src) {
|
||||||
// no-op
|
Mob* self = (Mob*)npc;
|
||||||
}
|
|
||||||
|
|
||||||
void Mob::onRoamStart() {
|
self->hp = self->maxHealth;
|
||||||
hp = maxHealth;
|
self->killedTime = 0;
|
||||||
killedTime = 0;
|
self->nextAttack = 0;
|
||||||
nextAttack = 0;
|
self->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(id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]);
|
pwr.handle(self->id, targetData, 110, Abilities::SkillTable[110].durationTime[0], Abilities::SkillTable[110].powerIntensity[0]);
|
||||||
// clear outlying debuffs
|
// clear outlying debuffs
|
||||||
clearDebuff(this);
|
clearDebuff(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mob::onCombatStart(EntityRef src) {
|
void MobAI::onCombatStart(CombatNPC* npc, EntityRef src) {
|
||||||
|
Mob* self = (Mob*)npc;
|
||||||
|
|
||||||
assert(src.kind == EntityKind::PLAYER);
|
assert(src.kind == EntityKind::PLAYER);
|
||||||
target = src.sock;
|
self->target = src.sock;
|
||||||
nextMovement = getTime();
|
self->nextMovement = getTime();
|
||||||
nextAttack = 0;
|
self->nextAttack = 0;
|
||||||
|
|
||||||
roamX = x;
|
self->roamX = self->x;
|
||||||
roamY = y;
|
self->roamY = self->y;
|
||||||
roamZ = z;
|
self->roamZ = self->z;
|
||||||
|
|
||||||
int skillID = (int)data["m_iPassiveBuff"]; // cast passive
|
int skillID = (int)self->data["m_iPassiveBuff"]; // cast passive
|
||||||
std::vector<int> targetData = { 1, id, 0, 0, 0 };
|
std::vector<int> targetData = { 1, self->id, 0, 0, 0 };
|
||||||
for (auto& pwr : Abilities::Powers)
|
for (auto& pwr : Abilities::Powers)
|
||||||
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
|
if (pwr.skillType == Abilities::SkillTable[skillID].skillType)
|
||||||
pwr.handle(id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
|
pwr.handle(self->id, targetData, skillID, Abilities::SkillTable[skillID].durationTime[0], Abilities::SkillTable[skillID].powerIntensity[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mob::onRetreat() {
|
void MobAI::onRetreat(CombatNPC* npc, EntityRef src) {
|
||||||
target = nullptr;
|
Mob* self = (Mob*)npc;
|
||||||
MobAI::clearDebuff(this);
|
|
||||||
if (groupLeader != 0)
|
self->target = nullptr;
|
||||||
MobAI::groupRetreat(this);
|
MobAI::clearDebuff(self);
|
||||||
|
if (self->groupLeader != 0)
|
||||||
|
MobAI::groupRetreat(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mob::onDeath(EntityRef src) {
|
void MobAI::onDeath(CombatNPC* npc, EntityRef src) {
|
||||||
target = nullptr;
|
Mob* self = (Mob*)npc;
|
||||||
cbf = 0;
|
|
||||||
skillStyle = -1;
|
self->target = nullptr;
|
||||||
unbuffTimes.clear();
|
self->cbf = 0;
|
||||||
killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step?
|
self->skillStyle = -1;
|
||||||
|
self->unbuffTimes.clear();
|
||||||
|
self->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
|
// check for the edge case where hitting the mob did not aggro it
|
||||||
if (src.kind == EntityKind::PLAYER && src.isValid()) {
|
if (src.kind == EntityKind::PLAYER && src.isValid()) {
|
||||||
@ -787,8 +811,8 @@ void Mob::onDeath(EntityRef src) {
|
|||||||
Combat::genQItemRolls(leader, qitemRolls);
|
Combat::genQItemRolls(leader, qitemRolls);
|
||||||
|
|
||||||
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
|
if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) {
|
||||||
Items::giveMobDrop(src.sock, this, rolled, eventRolled);
|
Items::giveMobDrop(src.sock, self, rolled, eventRolled);
|
||||||
Missions::mobKilled(src.sock, type, qitemRolls);
|
Missions::mobKilled(src.sock, self->type, qitemRolls);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (int i = 0; i < leader->groupCnt; i++) {
|
for (int i = 0; i < leader->groupCnt; i++) {
|
||||||
@ -803,21 +827,21 @@ void Mob::onDeath(EntityRef src) {
|
|||||||
if (dist > 5000)
|
if (dist > 5000)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Items::giveMobDrop(sockTo, this, rolled, eventRolled);
|
Items::giveMobDrop(sockTo, self, rolled, eventRolled);
|
||||||
Missions::mobKilled(sockTo, type, qitemRolls);
|
Missions::mobKilled(sockTo, self->type, qitemRolls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delay the despawn animation
|
// delay the despawn animation
|
||||||
despawned = false;
|
self->despawned = false;
|
||||||
|
|
||||||
auto it = Transport::NPCQueues.find(id);
|
auto it = Transport::NPCQueues.find(self->id);
|
||||||
if (it == Transport::NPCQueues.end() || it->second.empty())
|
if (it == Transport::NPCQueues.end() || it->second.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// rewind or empty the movement queue
|
// rewind or empty the movement queue
|
||||||
if (staticPath) {
|
if (self->staticPath) {
|
||||||
/*
|
/*
|
||||||
* This is inelegant, but we wind forward in the path until we find the point that
|
* This is inelegant, but we wind forward in the path until we find the point that
|
||||||
* corresponds with the Mob's spawn point.
|
* corresponds with the Mob's spawn point.
|
||||||
@ -825,12 +849,12 @@ void Mob::onDeath(EntityRef src) {
|
|||||||
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
|
* IMPORTANT: The check in TableData::loadPaths() must pass or else this will loop forever.
|
||||||
*/
|
*/
|
||||||
auto& queue = it->second;
|
auto& queue = it->second;
|
||||||
for (auto point = queue.front(); point.x != spawnX || point.y != spawnY; point = queue.front()) {
|
for (auto point = queue.front(); point.x != self->spawnX || point.y != self->spawnY; point = queue.front()) {
|
||||||
queue.pop();
|
queue.pop();
|
||||||
queue.push(point);
|
queue.push(point);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Transport::NPCQueues.erase(id);
|
Transport::NPCQueues.erase(self->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,20 @@
|
|||||||
#include "NPCManager.hpp"
|
#include "NPCManager.hpp"
|
||||||
#include "Entities.hpp"
|
#include "Entities.hpp"
|
||||||
|
|
||||||
|
/* kill me */
|
||||||
|
struct Mob;
|
||||||
|
namespace MobAI {
|
||||||
|
void deadStep(CombatNPC* self, time_t currTime);
|
||||||
|
void combatStep(CombatNPC* self, time_t currTime);
|
||||||
|
void roamingStep(CombatNPC* self, time_t currTime);
|
||||||
|
void retreatStep(CombatNPC* self, time_t currTime);
|
||||||
|
|
||||||
|
void onRoamStart(CombatNPC* self, EntityRef src);
|
||||||
|
void onCombatStart(CombatNPC* self, EntityRef src);
|
||||||
|
void onRetreat(CombatNPC* self, EntityRef src);
|
||||||
|
void onDeath(CombatNPC* self, EntityRef src);
|
||||||
|
}
|
||||||
|
|
||||||
struct Mob : public CombatNPC {
|
struct Mob : public CombatNPC {
|
||||||
// general
|
// general
|
||||||
std::unordered_map<int32_t,time_t> unbuffTimes = {};
|
std::unordered_map<int32_t,time_t> unbuffTimes = {};
|
||||||
@ -61,6 +75,17 @@ struct Mob : public CombatNPC {
|
|||||||
hp = maxHealth;
|
hp = maxHealth;
|
||||||
|
|
||||||
kind = EntityKind::MOB;
|
kind = EntityKind::MOB;
|
||||||
|
|
||||||
|
// AI
|
||||||
|
stateHandlers[AIState::DEAD] = MobAI::deadStep;
|
||||||
|
stateHandlers[AIState::COMBAT] = MobAI::combatStep;
|
||||||
|
stateHandlers[AIState::ROAMING] = MobAI::roamingStep;
|
||||||
|
stateHandlers[AIState::RETREAT] = MobAI::retreatStep;
|
||||||
|
|
||||||
|
transitionHandlers[AIState::DEAD] = MobAI::onDeath;
|
||||||
|
transitionHandlers[AIState::COMBAT] = MobAI::onCombatStart;
|
||||||
|
transitionHandlers[AIState::ROAMING] = MobAI::onRoamStart;
|
||||||
|
transitionHandlers[AIState::RETREAT] = MobAI::onRetreat;
|
||||||
}
|
}
|
||||||
|
|
||||||
// constructor for /summon
|
// constructor for /summon
|
||||||
@ -71,16 +96,7 @@ struct Mob : public CombatNPC {
|
|||||||
|
|
||||||
~Mob() {}
|
~Mob() {}
|
||||||
|
|
||||||
virtual void roamingStep(time_t currTime) override;
|
virtual void step(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];
|
||||||
|
Loading…
Reference in New Issue
Block a user