2018-02-20 22:46:44 +01:00
|
|
|
#include <common/Util/Util.h>
|
|
|
|
#include <common/Util/UtilMath.h>
|
|
|
|
#include <common/Network/PacketContainer.h>
|
|
|
|
#include <common/Exd/ExdDataGenerated.h>
|
|
|
|
#include <common/Network/GamePacket.h>
|
|
|
|
|
|
|
|
#include "Forwards.h"
|
|
|
|
#include "Action/Action.h"
|
|
|
|
|
|
|
|
#include "Zone/Zone.h"
|
|
|
|
|
|
|
|
#include "Network/GameConnection.h"
|
|
|
|
#include "Network/PacketWrappers/ActorControlPacket142.h"
|
|
|
|
#include "Network/PacketWrappers/ActorControlPacket143.h"
|
|
|
|
#include "Network/PacketWrappers/ActorControlPacket144.h"
|
|
|
|
#include "Network/PacketWrappers/UpdateHpMpTpPacket.h"
|
|
|
|
|
|
|
|
#include "StatusEffect/StatusEffect.h"
|
|
|
|
#include "Action/ActionCollision.h"
|
|
|
|
#include "ServerZone.h"
|
|
|
|
#include "Session.h"
|
|
|
|
#include "Math/CalcBattle.h"
|
|
|
|
#include "Chara.h"
|
|
|
|
#include "Player.h"
|
|
|
|
#include "Zone/TerritoryMgr.h"
|
|
|
|
|
|
|
|
extern Core::ServerZone g_serverZone;
|
|
|
|
extern Core::Data::ExdDataGenerated g_exdDataGen;
|
|
|
|
extern Core::TerritoryMgr g_territoryMgr;
|
|
|
|
|
|
|
|
using namespace Core::Common;
|
|
|
|
using namespace Core::Network::Packets;
|
|
|
|
using namespace Core::Network::Packets::Server;
|
|
|
|
|
|
|
|
Core::Entity::Chara::Chara( ObjKind type ) :
|
|
|
|
Actor( type )
|
|
|
|
{
|
|
|
|
// initialize the free slot queue
|
|
|
|
for( uint8_t i = 0; i < MAX_STATUS_EFFECTS; i++ )
|
|
|
|
{
|
|
|
|
m_statusEffectFreeSlotQueue.push( i );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Core::Entity::Chara::~Chara()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return the actors name */
|
|
|
|
std::string Core::Entity::Chara::getName() const
|
|
|
|
{
|
|
|
|
return std::string( m_name );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*! \return current stance of the actors */
|
|
|
|
Core::Entity::Chara::Stance Core::Entity::Chara::getStance() const
|
|
|
|
{
|
|
|
|
return m_currentStance;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return actor stats */
|
|
|
|
Core::Entity::Chara::ActorStats Core::Entity::Chara::getStats() const
|
|
|
|
{
|
|
|
|
return m_baseStats;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current HP */
|
|
|
|
uint32_t Core::Entity::Chara::getHp() const
|
|
|
|
{
|
|
|
|
return m_hp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current MP */
|
|
|
|
uint32_t Core::Entity::Chara::getMp() const
|
|
|
|
{
|
|
|
|
return m_mp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current TP */
|
|
|
|
uint16_t Core::Entity::Chara::getTp() const
|
|
|
|
{
|
|
|
|
return m_tp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current GP */
|
|
|
|
uint16_t Core::Entity::Chara::getGp() const
|
|
|
|
{
|
|
|
|
return m_gp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current invincibility type */
|
|
|
|
InvincibilityType Core::Entity::Chara::getInvincibilityType() const
|
|
|
|
{
|
|
|
|
return m_invincibilityType;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current class or job */
|
|
|
|
Core::Common::ClassJob Core::Entity::Chara::getClass() const
|
|
|
|
{
|
|
|
|
return m_class;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current class or job as int32_t ( this feels pointless ) */
|
|
|
|
uint8_t Core::Entity::Chara::getClassAsInt() const
|
|
|
|
{
|
|
|
|
return static_cast< uint8_t >( m_class );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param ClassJob to set */
|
|
|
|
void Core::Entity::Chara::setClass( Common::ClassJob classJob )
|
|
|
|
{
|
|
|
|
m_class = classJob;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param Id of the target to set */
|
|
|
|
void Core::Entity::Chara::setTargetId( uint64_t targetId )
|
|
|
|
{
|
|
|
|
m_targetId = targetId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return Id of the current target */
|
|
|
|
uint64_t Core::Entity::Chara::getTargetId() const
|
|
|
|
{
|
|
|
|
return m_targetId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return True if the actor is alive */
|
|
|
|
bool Core::Entity::Chara::isAlive() const
|
|
|
|
{
|
|
|
|
return ( m_hp > 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return max hp for the actor */
|
|
|
|
uint32_t Core::Entity::Chara::getMaxHp() const
|
|
|
|
{
|
|
|
|
return m_baseStats.max_hp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return max mp for the actor */
|
|
|
|
uint32_t Core::Entity::Chara::getMaxMp() const
|
|
|
|
{
|
|
|
|
return m_baseStats.max_mp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return reset hp to current max hp */
|
|
|
|
void Core::Entity::Chara::resetHp()
|
|
|
|
{
|
|
|
|
m_hp = getMaxHp();
|
|
|
|
sendStatusUpdate( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return reset mp to current max mp */
|
|
|
|
void Core::Entity::Chara::resetMp()
|
|
|
|
{
|
|
|
|
m_mp = getMaxMp();
|
|
|
|
sendStatusUpdate( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param hp amount to set ( caps to maxHp ) */
|
|
|
|
void Core::Entity::Chara::setHp( uint32_t hp )
|
|
|
|
{
|
|
|
|
m_hp = hp < getMaxHp() ? hp : getMaxHp();
|
|
|
|
sendStatusUpdate( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param mp amount to set ( caps to maxMp ) */
|
|
|
|
void Core::Entity::Chara::setMp( uint32_t mp )
|
|
|
|
{
|
|
|
|
m_mp = mp < getMaxMp() ? mp : getMaxMp();
|
|
|
|
sendStatusUpdate( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param gp amount to set*/
|
|
|
|
void Core::Entity::Chara::setGp( uint32_t gp )
|
|
|
|
{
|
|
|
|
m_gp = gp;
|
|
|
|
sendStatusUpdate( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param type invincibility type to set */
|
|
|
|
void Core::Entity::Chara::setInvincibilityType( Common::InvincibilityType type )
|
|
|
|
{
|
|
|
|
m_invincibilityType = type;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return current status of the actor */
|
|
|
|
Core::Entity::Chara::ActorStatus Core::Entity::Chara::getStatus() const
|
|
|
|
{
|
|
|
|
return m_status;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param status to set */
|
|
|
|
void Core::Entity::Chara::setStatus( ActorStatus status )
|
|
|
|
{
|
|
|
|
m_status = status;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Performs necessary steps to mark an actor dead.
|
|
|
|
Sets hp/mp/tp, sets status, plays animation and fires onDeath event
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::die()
|
|
|
|
{
|
|
|
|
m_status = ActorStatus::Dead;
|
|
|
|
m_hp = 0;
|
|
|
|
m_mp = 0;
|
|
|
|
m_tp = 0;
|
|
|
|
|
|
|
|
// fire onDeath event
|
|
|
|
onDeath();
|
|
|
|
|
|
|
|
// if the actor is a player, the update needs to be send to himself too
|
|
|
|
bool selfNeedsUpdate = isPlayer();
|
|
|
|
|
|
|
|
sendToInRangeSet( ActorControlPacket142( m_id, SetStatus, static_cast< uint8_t >( ActorStatus::Dead ) ), selfNeedsUpdate );
|
|
|
|
|
|
|
|
// TODO: not all actor show the death animation when they die, some quest npcs might just despawn
|
|
|
|
// although that might be handled by setting the HP to 1 and doing some script magic
|
|
|
|
sendToInRangeSet( ActorControlPacket142( m_id, DeathAnimation, 0, 0, 0, 0x20 ), selfNeedsUpdate );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Calculates and sets the rotation to look towards a specified
|
|
|
|
position
|
|
|
|
|
|
|
|
\param Position to look towards
|
|
|
|
*/
|
|
|
|
bool Core::Entity::Chara::face( const Common::FFXIVARR_POSITION3& p )
|
|
|
|
{
|
2018-02-22 18:12:36 +01:00
|
|
|
float oldRot = getRot();
|
2018-02-20 22:46:44 +01:00
|
|
|
float rot = Math::Util::calcAngFrom( getPos().x, getPos().z, p.x, p.z );
|
|
|
|
float newRot = PI - rot + ( PI / 2 );
|
|
|
|
|
|
|
|
m_pCell = nullptr;
|
|
|
|
|
2018-02-22 18:12:36 +01:00
|
|
|
setRot( newRot );
|
2018-02-20 22:46:44 +01:00
|
|
|
|
|
|
|
return oldRot != newRot ? true : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Set and propagate the actor stance to in range players
|
|
|
|
( not the actor himself )
|
|
|
|
|
|
|
|
\param stance to set
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::setStance( Stance stance )
|
|
|
|
{
|
|
|
|
m_currentStance = stance;
|
|
|
|
|
|
|
|
sendToInRangeSet( ActorControlPacket142( m_id, ToggleAggro, stance, 1 ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Check if an action is queued for execution, if so update it
|
|
|
|
and if fully performed, clean up again.
|
|
|
|
|
|
|
|
\return true if a queued action has been updated
|
|
|
|
*/
|
|
|
|
bool Core::Entity::Chara::checkAction()
|
|
|
|
{
|
|
|
|
|
|
|
|
if( m_pCurrentAction == nullptr )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( m_pCurrentAction->update() )
|
|
|
|
m_pCurrentAction.reset();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Change the current target and propagate to in range players
|
|
|
|
|
|
|
|
\param target actor id
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::changeTarget( uint64_t targetId )
|
|
|
|
{
|
|
|
|
setTargetId( targetId );
|
|
|
|
sendToInRangeSet( ActorControlPacket144( m_id, SetTarget, 0, 0, 0, 0, targetId ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Dummy function \return 0
|
|
|
|
*/
|
|
|
|
uint8_t Core::Entity::Chara::getLevel() const
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Let an actor take damage and perform necessary steps
|
|
|
|
according to resulting hp, propagates new hp value to players
|
|
|
|
in range
|
|
|
|
TODO: eventually this needs to distinguish between physical and
|
|
|
|
magical dmg and take status effects into account
|
|
|
|
|
|
|
|
\param amount of damage to be taken
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::takeDamage( uint32_t damage )
|
|
|
|
{
|
|
|
|
if( damage >= m_hp )
|
|
|
|
{
|
|
|
|
switch( m_invincibilityType ) {
|
|
|
|
case InvincibilityNone:
|
|
|
|
setHp( 0 );
|
|
|
|
die();
|
|
|
|
break;
|
|
|
|
case InvincibilityRefill:
|
|
|
|
resetHp();
|
|
|
|
break;
|
|
|
|
case InvincibilityStayAlive:
|
|
|
|
setHp( 0 );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
m_hp -= damage;
|
|
|
|
|
|
|
|
sendStatusUpdate( false );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Let an actor get healed and perform necessary steps
|
|
|
|
according to resulting hp, propagates new hp value to players
|
|
|
|
in range
|
|
|
|
|
|
|
|
\param amount of hp to be healed
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::heal( uint32_t amount )
|
|
|
|
{
|
|
|
|
if( ( m_hp + amount ) > getMaxHp() )
|
|
|
|
{
|
|
|
|
m_hp = getMaxHp();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
m_hp += amount;
|
|
|
|
|
|
|
|
sendStatusUpdate( false );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Send an HpMpTp update to players in range ( and potentially to self )
|
|
|
|
TODO: poor naming, should be changed. Status is not HP. Also should be virtual
|
|
|
|
so players can have their own version and we can abolish the param.
|
|
|
|
|
|
|
|
\param true if the update should also be sent to the actor ( player ) himself
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::sendStatusUpdate( bool toSelf )
|
|
|
|
{
|
|
|
|
UpdateHpMpTpPacket updateHpPacket( *this );
|
|
|
|
sendToInRangeSet( updateHpPacket );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \return ActionPtr of the currently registered action, or nullptr */
|
|
|
|
Core::Action::ActionPtr Core::Entity::Chara::getCurrentAction() const
|
|
|
|
{
|
|
|
|
return m_pCurrentAction;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param ActionPtr of the action to be registered */
|
|
|
|
void Core::Entity::Chara::setCurrentAction( Core::Action::ActionPtr pAction )
|
|
|
|
{
|
|
|
|
m_pCurrentAction = pAction;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Autoattack prototype implementation
|
|
|
|
TODO: move the check if the autoAttack can be performed to the callee
|
|
|
|
also rename autoAttack to autoAttack as that is more elaborate
|
|
|
|
On top of that, this only solves attacks from melee classes.
|
|
|
|
Will have to be extended for ranged attacks.
|
|
|
|
|
|
|
|
\param ActorPtr the autoAttack is performed on
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::autoAttack( CharaPtr pTarget )
|
|
|
|
{
|
|
|
|
|
|
|
|
uint64_t tick = Util::getTimeMs();
|
|
|
|
|
|
|
|
if( ( tick - m_lastAttack ) > 2500 )
|
|
|
|
{
|
|
|
|
pTarget->onActionHostile( *this );
|
|
|
|
m_lastAttack = tick;
|
|
|
|
srand( static_cast< uint32_t >( tick ) );
|
|
|
|
|
|
|
|
uint16_t damage = static_cast< uint16_t >( 10 + rand() % 12 );
|
|
|
|
uint32_t variation = static_cast< uint32_t >( 0 + rand() % 4 );
|
|
|
|
|
|
|
|
ZoneChannelPacket< FFXIVIpcEffect > effectPacket( getId() );
|
|
|
|
effectPacket.data().targetId = pTarget->getId();
|
|
|
|
effectPacket.data().actionAnimationId = 0x366;
|
|
|
|
effectPacket.data().unknown_2 = variation;
|
|
|
|
// effectPacket.data().unknown_3 = 1;
|
|
|
|
effectPacket.data().actionTextId = 0x366;
|
|
|
|
effectPacket.data().numEffects = 1;
|
2018-02-22 18:12:36 +01:00
|
|
|
effectPacket.data().rotation = Math::Util::floatToUInt16Rot( getRot() );
|
2018-02-20 22:46:44 +01:00
|
|
|
effectPacket.data().effectTarget = pTarget->getId();
|
|
|
|
effectPacket.data().effects[0].value = damage;
|
|
|
|
effectPacket.data().effects[0].effectType = ActionEffectType::Damage;
|
|
|
|
effectPacket.data().effects[0].hitSeverity = static_cast< ActionHitSeverityType >( variation );
|
|
|
|
effectPacket.data().effects[0].unknown_3 = 7;
|
|
|
|
|
|
|
|
sendToInRangeSet( effectPacket );
|
|
|
|
|
|
|
|
if( isPlayer() )
|
|
|
|
getAsPlayer()->queuePacket( effectPacket );
|
|
|
|
|
|
|
|
pTarget->takeDamage( damage );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
ChaiScript Skill Handler.
|
|
|
|
|
|
|
|
\param GamePacketPtr to send
|
|
|
|
\param bool should be send to self?
|
|
|
|
*/
|
|
|
|
void Core::Entity::Chara::handleScriptSkill( uint32_t type, uint16_t actionId, uint64_t param1,
|
|
|
|
uint64_t param2, Entity::Chara& target )
|
|
|
|
{
|
|
|
|
|
|
|
|
if( isPlayer() )
|
|
|
|
{
|
|
|
|
getAsPlayer()->sendDebug( std::to_string( target.getId() ) );
|
|
|
|
getAsPlayer()->sendDebug( "Handle script skill type: " + std::to_string( type ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
auto actionInfoPtr = g_exdDataGen.get< Core::Data::Action >( actionId );
|
|
|
|
|
|
|
|
// Todo: Effect packet generator. 90% of this is basically setting params and it's basically unreadable.
|
|
|
|
// Prepare packet. This is seemingly common for all packets in the action handler.
|
|
|
|
|
|
|
|
ZoneChannelPacket< FFXIVIpcEffect > effectPacket( getId() );
|
|
|
|
effectPacket.data().targetId = target.getId();
|
|
|
|
effectPacket.data().actionAnimationId = actionId;
|
|
|
|
effectPacket.data().unknown_62 = 1; // Affects displaying action name next to number in floating text
|
|
|
|
effectPacket.data().unknown_2 = 1; // This seems to have an effect on the "double-cast finish" animation
|
|
|
|
effectPacket.data().actionTextId = actionId;
|
|
|
|
effectPacket.data().numEffects = 1;
|
2018-02-22 18:12:36 +01:00
|
|
|
effectPacket.data().rotation = Math::Util::floatToUInt16Rot( getRot() );
|
2018-02-20 22:46:44 +01:00
|
|
|
effectPacket.data().effectTarget = target.getId();
|
|
|
|
|
|
|
|
// Todo: for each actor, calculate how much damage the calculated value should deal to them - 2-step damage calc. we only have 1-step
|
|
|
|
switch( type )
|
|
|
|
{
|
|
|
|
|
|
|
|
case ActionEffectType::Damage:
|
|
|
|
{
|
|
|
|
effectPacket.data().effects[0].value = static_cast< uint16_t >( param1 );
|
|
|
|
effectPacket.data().effects[0].effectType = ActionEffectType::Damage;
|
|
|
|
effectPacket.data().effects[0].hitSeverity = ActionHitSeverityType::NormalDamage;
|
|
|
|
effectPacket.data().effects[0].unknown_3 = 7;
|
|
|
|
|
|
|
|
if( actionInfoPtr->castType == 1 && actionInfoPtr->effectRange != 0 || actionInfoPtr->castType != 1 )
|
|
|
|
{
|
|
|
|
// If action on this specific target is valid...
|
|
|
|
if ( isPlayer() && !ActionCollision::isActorApplicable( target, TargetFilter::Enemies ) )
|
|
|
|
break;
|
|
|
|
|
|
|
|
sendToInRangeSet( effectPacket, true );
|
|
|
|
|
|
|
|
if ( target.isAlive() )
|
|
|
|
target.onActionHostile( *this );
|
|
|
|
|
|
|
|
target.takeDamage( static_cast< uint32_t >( param1 ) );
|
|
|
|
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
|
2018-02-22 15:31:10 +01:00
|
|
|
auto actorsCollided = ActionCollision::getActorsHitFromAction( target.getPos(), getInRangeActors( true ),
|
2018-02-20 22:46:44 +01:00
|
|
|
actionInfoPtr, TargetFilter::Enemies );
|
|
|
|
|
|
|
|
for( const auto& pHitActor : actorsCollided )
|
|
|
|
{
|
|
|
|
effectPacket.data().targetId = pHitActor->getId();
|
|
|
|
effectPacket.data().effectTarget = pHitActor->getId();
|
|
|
|
|
|
|
|
// todo: send to range of what? ourselves? when mob script hits this is going to be lacking
|
|
|
|
sendToInRangeSet( effectPacket, true );
|
|
|
|
|
|
|
|
|
2018-02-22 15:31:10 +01:00
|
|
|
if( pHitActor->getAsChara()->isAlive() )
|
|
|
|
pHitActor->getAsChara()->onActionHostile( *this );
|
2018-02-20 22:46:44 +01:00
|
|
|
|
2018-02-22 15:31:10 +01:00
|
|
|
pHitActor->getAsChara()->takeDamage( static_cast< uint32_t >( param1 ) );
|
2018-02-20 22:46:44 +01:00
|
|
|
|
|
|
|
// Debug
|
|
|
|
if ( isPlayer() )
|
|
|
|
{
|
|
|
|
if ( pHitActor->isPlayer() )
|
2018-02-22 15:31:10 +01:00
|
|
|
getAsPlayer()->sendDebug( "AoE hit actor " + std::to_string( pHitActor->getId() ) +
|
|
|
|
" (" + pHitActor->getAsChara()->getName() + ")" );
|
2018-02-20 22:46:44 +01:00
|
|
|
else
|
|
|
|
getAsPlayer()->sendDebug( "AoE hit actor " + std::to_string( pHitActor->getId() ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case ActionEffectType::Heal:
|
|
|
|
{
|
|
|
|
uint32_t calculatedHeal = Math::CalcBattle::calculateHealValue( getAsPlayer(), static_cast< uint32_t >( param1 ) );
|
|
|
|
|
|
|
|
effectPacket.data().effects[0].value = calculatedHeal;
|
|
|
|
effectPacket.data().effects[0].effectType = ActionEffectType::Heal;
|
|
|
|
effectPacket.data().effects[0].hitSeverity = ActionHitSeverityType::NormalHeal;
|
|
|
|
|
|
|
|
if( actionInfoPtr->castType == 1 && actionInfoPtr->effectRange != 0 || actionInfoPtr->castType != 1 )
|
|
|
|
{
|
|
|
|
if( isPlayer() && !ActionCollision::isActorApplicable( target, TargetFilter::Allies ) )
|
|
|
|
break;
|
|
|
|
|
|
|
|
sendToInRangeSet( effectPacket, true );
|
|
|
|
target.heal( calculatedHeal );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// todo: get proper packets: the following was just kind of thrown together from what we know.
|
|
|
|
// atm buggy (packets look "delayed" from client)
|
|
|
|
|
2018-02-22 15:31:10 +01:00
|
|
|
auto actorsCollided = ActionCollision::getActorsHitFromAction( target.getPos(), getInRangeActors(true),
|
2018-02-20 22:46:44 +01:00
|
|
|
actionInfoPtr, TargetFilter::Allies );
|
|
|
|
|
|
|
|
for( auto pHitActor : actorsCollided )
|
|
|
|
{
|
|
|
|
effectPacket.data().targetId = target.getId();
|
|
|
|
effectPacket.data().effectTarget = pHitActor->getId();
|
|
|
|
|
|
|
|
sendToInRangeSet( effectPacket, true );
|
2018-02-22 15:31:10 +01:00
|
|
|
pHitActor->getAsChara()->heal( calculatedHeal );
|
2018-02-20 22:46:44 +01:00
|
|
|
|
|
|
|
// Debug
|
|
|
|
if( isPlayer() )
|
|
|
|
{
|
|
|
|
if( pHitActor->isPlayer() )
|
2018-02-22 15:31:10 +01:00
|
|
|
getAsPlayer()->sendDebug( "AoE hit actor " + std::to_string( pHitActor->getId() ) +
|
|
|
|
" (" + pHitActor->getAsChara()->getName() + ")" );
|
2018-02-20 22:46:44 +01:00
|
|
|
else
|
|
|
|
getAsPlayer()->sendDebug( "AoE hit actor " + std::to_string( pHitActor->getId() ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param StatusEffectPtr to be applied to the actor */
|
|
|
|
void Core::Entity::Chara::addStatusEffect( StatusEffect::StatusEffectPtr pEffect )
|
|
|
|
{
|
|
|
|
int8_t nextSlot = getStatusEffectFreeSlot();
|
|
|
|
// if there is no slot left, do not add the effect
|
|
|
|
if( nextSlot == -1 )
|
|
|
|
return;
|
|
|
|
|
|
|
|
pEffect->applyStatus();
|
|
|
|
m_statusEffectMap[nextSlot] = pEffect;
|
|
|
|
|
|
|
|
ZoneChannelPacket< Server::FFXIVIpcAddStatusEffect > statusEffectAdd( getId() );
|
|
|
|
statusEffectAdd.data().actor_id = pEffect->getTargetActorId();
|
|
|
|
statusEffectAdd.data().actor_id1 = pEffect->getSrcActorId();
|
|
|
|
statusEffectAdd.data().current_hp = getHp();
|
|
|
|
statusEffectAdd.data().current_mp = getMp();
|
|
|
|
statusEffectAdd.data().current_tp = getTp();
|
|
|
|
statusEffectAdd.data().duration = static_cast< float >( pEffect->getDuration() ) / 1000;
|
|
|
|
statusEffectAdd.data().effect_id = pEffect->getId();
|
|
|
|
statusEffectAdd.data().effect_index = nextSlot;
|
|
|
|
statusEffectAdd.data().max_hp = getMaxHp();
|
|
|
|
statusEffectAdd.data().max_mp = getMaxMp();
|
|
|
|
statusEffectAdd.data().max_something = 1;
|
|
|
|
//statusEffectAdd.data().unknown2 = 28;
|
|
|
|
statusEffectAdd.data().param = pEffect->getParam();
|
|
|
|
|
|
|
|
sendToInRangeSet( statusEffectAdd, isPlayer() );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param StatusEffectPtr to be applied to the actor */
|
|
|
|
void Core::Entity::Chara::addStatusEffectById( uint32_t id, int32_t duration, Entity::Chara& source, uint16_t param )
|
|
|
|
{
|
|
|
|
auto effect = StatusEffect::make_StatusEffect( id, source.getAsChara(), getAsChara(), duration, 3000 );
|
|
|
|
effect->setParam( param );
|
|
|
|
addStatusEffect( effect );
|
|
|
|
}
|
|
|
|
|
|
|
|
/*! \param StatusEffectPtr to be applied to the actor */
|
|
|
|
void Core::Entity::Chara::addStatusEffectByIdIfNotExist( uint32_t id, int32_t duration, Entity::Chara& source, uint16_t param )
|
|
|
|
{
|
|
|
|
if( hasStatusEffect( id ) )
|
|
|
|
return;
|
|
|
|
|
|
|
|
auto effect = StatusEffect::make_StatusEffect( id, source.getAsChara(), getAsChara(), duration, 3000 );
|
|
|
|
effect->setParam( param );
|
|
|
|
addStatusEffect( effect );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
int8_t Core::Entity::Chara::getStatusEffectFreeSlot()
|
|
|
|
{
|
|
|
|
int8_t freeEffectSlot = -1;
|
|
|
|
|
|
|
|
if( m_statusEffectFreeSlotQueue.empty() )
|
|
|
|
return freeEffectSlot;
|
|
|
|
|
|
|
|
freeEffectSlot = m_statusEffectFreeSlotQueue.front();
|
|
|
|
m_statusEffectFreeSlotQueue.pop();
|
|
|
|
|
|
|
|
return freeEffectSlot;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Core::Entity::Chara::statusEffectFreeSlot( uint8_t slotId )
|
|
|
|
{
|
|
|
|
m_statusEffectFreeSlotQueue.push( slotId );
|
|
|
|
}
|
|
|
|
|
|
|
|
void Core::Entity::Chara::removeSingleStatusEffectById( uint32_t id )
|
|
|
|
{
|
|
|
|
for( auto effectIt : m_statusEffectMap )
|
|
|
|
{
|
|
|
|
if( effectIt.second->getId() == id )
|
|
|
|
{
|
|
|
|
removeStatusEffect( effectIt.first );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Core::Entity::Chara::removeStatusEffect( uint8_t effectSlotId )
|
|
|
|
{
|
|
|
|
auto pEffectIt = m_statusEffectMap.find( effectSlotId );
|
|
|
|
if( pEffectIt == m_statusEffectMap.end() )
|
|
|
|
return;
|
|
|
|
|
|
|
|
statusEffectFreeSlot( effectSlotId );
|
|
|
|
|
|
|
|
auto pEffect = pEffectIt->second;
|
|
|
|
pEffect->removeStatus();
|
|
|
|
|
|
|
|
sendToInRangeSet( ActorControlPacket142( getId(), StatusEffectLose, pEffect->getId() ), isPlayer() );
|
|
|
|
|
|
|
|
m_statusEffectMap.erase( effectSlotId );
|
|
|
|
|
|
|
|
sendStatusEffectUpdate();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::map< uint8_t, Core::StatusEffect::StatusEffectPtr > Core::Entity::Chara::getStatusEffectMap() const
|
|
|
|
{
|
|
|
|
return m_statusEffectMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Core::Entity::Chara::sendStatusEffectUpdate()
|
|
|
|
{
|
|
|
|
uint64_t currentTimeMs = Util::getTimeMs();
|
|
|
|
|
|
|
|
ZoneChannelPacket< Server::FFXIVIpcStatusEffectList > statusEffectList( getId() );
|
|
|
|
|
|
|
|
statusEffectList.data().current_hp = getHp();
|
|
|
|
statusEffectList.data().current_mp = getMp();
|
|
|
|
statusEffectList.data().currentTp = getTp();
|
|
|
|
statusEffectList.data().max_hp = getMaxHp();
|
|
|
|
statusEffectList.data().max_mp = getMaxMp();
|
|
|
|
uint8_t slot = 0;
|
|
|
|
for( auto effectIt : m_statusEffectMap )
|
|
|
|
{
|
|
|
|
float timeLeft = static_cast< float >( effectIt.second->getDuration() -
|
|
|
|
( currentTimeMs - effectIt.second->getStartTimeMs() ) ) / 1000;
|
|
|
|
statusEffectList.data().effect[slot].duration = timeLeft;
|
|
|
|
statusEffectList.data().effect[slot].effect_id = effectIt.second->getId();
|
|
|
|
statusEffectList.data().effect[slot].sourceActorId = effectIt.second->getSrcActorId();
|
|
|
|
slot++;
|
|
|
|
}
|
|
|
|
|
|
|
|
sendToInRangeSet( statusEffectList, isPlayer() );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void Core::Entity::Chara::updateStatusEffects()
|
|
|
|
{
|
|
|
|
uint64_t currentTimeMs = Util::getTimeMs();
|
|
|
|
|
|
|
|
uint32_t thisTickDmg = 0;
|
|
|
|
uint32_t thisTickHeal = 0;
|
|
|
|
|
|
|
|
for( auto effectIt : m_statusEffectMap )
|
|
|
|
{
|
|
|
|
uint8_t effectIndex = effectIt.first;
|
|
|
|
auto effect = effectIt.second;
|
|
|
|
|
|
|
|
uint64_t lastTick = effect->getLastTickMs();
|
|
|
|
uint64_t startTime = effect->getStartTimeMs();
|
|
|
|
uint32_t duration = effect->getDuration();
|
|
|
|
uint32_t tickRate = effect->getTickRate();
|
|
|
|
|
|
|
|
if( ( currentTimeMs - startTime ) > duration )
|
|
|
|
{
|
|
|
|
// remove status effect
|
|
|
|
removeStatusEffect( effectIndex );
|
|
|
|
// break because removing invalidates iterators
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( ( currentTimeMs - lastTick ) > tickRate )
|
|
|
|
{
|
|
|
|
effect->setLastTick( currentTimeMs );
|
|
|
|
effect->onTick();
|
|
|
|
|
|
|
|
auto thisEffect = effect->getTickEffect();
|
|
|
|
|
|
|
|
switch( thisEffect.first )
|
|
|
|
{
|
|
|
|
|
|
|
|
case 1:
|
|
|
|
{
|
|
|
|
thisTickDmg += thisEffect.second;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 2:
|
|
|
|
{
|
|
|
|
thisTickHeal += thisEffect.second;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if( thisTickDmg != 0 )
|
|
|
|
{
|
|
|
|
takeDamage( thisTickDmg );
|
|
|
|
sendToInRangeSet( ActorControlPacket142( getId(), HPFloatingText, 0, static_cast< uint8_t >( ActionEffectType::Damage ), thisTickDmg ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( thisTickHeal != 0 )
|
|
|
|
{
|
|
|
|
heal( thisTickDmg );
|
|
|
|
sendToInRangeSet( ActorControlPacket142( getId(), HPFloatingText, 0, static_cast< uint8_t >( ActionEffectType::Heal ), thisTickHeal ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Core::Entity::Chara::hasStatusEffect( uint32_t id )
|
|
|
|
{
|
|
|
|
if( m_statusEffectMap.find( id ) != m_statusEffectMap.end() )
|
|
|
|
return true;
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-22 15:31:10 +01:00
|
|
|
|
|
|
|
Chara::ModelType Chara::getModelType() const
|
|
|
|
{
|
|
|
|
return m_modelType;
|
|
|
|
}
|