2017-08-08 13:53:47 +02:00
|
|
|
#include "Action.h"
|
|
|
|
|
2019-07-27 00:37:40 +10:00
|
|
|
#include <Inventory/Item.h>
|
|
|
|
|
2019-02-08 22:09:48 +11:00
|
|
|
#include <Exd/ExdDataGenerated.h>
|
2018-03-06 22:22:19 +01:00
|
|
|
#include <Util/Util.h>
|
2019-02-08 22:09:48 +11:00
|
|
|
#include "Script/ScriptMgr.h"
|
2017-08-08 13:53:47 +02:00
|
|
|
|
2019-03-23 11:48:49 +11:00
|
|
|
#include <Math/CalcStats.h>
|
|
|
|
|
2019-02-08 22:09:48 +11:00
|
|
|
#include "Actor/Player.h"
|
2019-02-11 10:36:17 +11:00
|
|
|
#include "Actor/BNpc.h"
|
2017-08-08 13:53:47 +02:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
#include "Action/ActionLut.h"
|
|
|
|
|
2019-07-21 22:33:33 +10:00
|
|
|
#include "Territory/Territory.h"
|
2019-02-11 11:47:04 +11:00
|
|
|
|
2019-02-08 22:09:48 +11:00
|
|
|
#include <Network/CommonActorControl.h>
|
2019-10-09 18:14:53 +02:00
|
|
|
#include "Network/PacketWrappers/ActorControlPacket.h"
|
|
|
|
#include "Network/PacketWrappers/ActorControlSelfPacket.h"
|
|
|
|
#include "Network/PacketWrappers/ActorControlTargetPacket.h"
|
2019-07-25 22:46:10 +10:00
|
|
|
|
2019-04-07 16:04:36 +10:00
|
|
|
#include <Logging/Logger.h>
|
2019-07-25 22:46:10 +10:00
|
|
|
|
2019-04-07 16:04:36 +10:00
|
|
|
#include <Util/ActorFilter.h>
|
2019-07-26 23:20:13 +10:00
|
|
|
#include <Util/UtilMath.h>
|
2020-03-01 01:00:57 +11:00
|
|
|
#include <Service.h>
|
2019-02-08 22:09:48 +11:00
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
using namespace Sapphire;
|
2019-02-08 22:09:48 +11:00
|
|
|
using namespace Sapphire::Common;
|
|
|
|
using namespace Sapphire::Network;
|
|
|
|
using namespace Sapphire::Network::Packets;
|
|
|
|
using namespace Sapphire::Network::Packets::Server;
|
|
|
|
using namespace Sapphire::Network::ActorControl;
|
2019-06-03 00:39:02 +10:00
|
|
|
using namespace Sapphire::World;
|
2019-02-08 22:09:48 +11:00
|
|
|
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
Action::Action::Action() = default;
|
|
|
|
Action::Action::~Action() = default;
|
2019-02-08 22:09:48 +11:00
|
|
|
|
2020-03-01 01:00:57 +11:00
|
|
|
Action::Action::Action( Entity::CharaPtr caster, uint32_t actionId, uint16_t sequence) :
|
|
|
|
Action( std::move( caster ), actionId, sequence, nullptr )
|
2019-03-07 21:58:12 +11:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-07-26 20:28:01 +10:00
|
|
|
Action::Action::Action( Entity::CharaPtr caster, uint32_t actionId, uint16_t sequence,
|
2020-03-01 01:00:57 +11:00
|
|
|
Data::ActionPtr actionData ) :
|
2019-06-02 20:45:18 +10:00
|
|
|
m_pSource( std::move( caster ) ),
|
|
|
|
m_actionData( std::move( actionData ) ),
|
|
|
|
m_id( actionId ),
|
|
|
|
m_targetId( 0 ),
|
|
|
|
m_startTime( 0 ),
|
2019-07-26 20:28:01 +10:00
|
|
|
m_interruptType( Common::ActionInterruptType::None ),
|
2023-03-07 05:12:18 +09:00
|
|
|
m_sequence( sequence ),
|
|
|
|
m_isAutoAttack( false ),
|
|
|
|
m_disableGenericHandler( false ),
|
|
|
|
m_shouldAlwaysCombo( false )
|
2019-03-07 21:58:12 +11:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
uint32_t Action::Action::getId() const
|
2019-03-07 21:58:12 +11:00
|
|
|
{
|
|
|
|
return m_id;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::init()
|
2017-08-08 13:53:47 +02:00
|
|
|
{
|
2019-03-07 21:58:12 +11:00
|
|
|
if( !m_actionData )
|
|
|
|
{
|
|
|
|
// need to get actionData
|
2020-03-01 01:00:57 +11:00
|
|
|
auto& exdData = Common::Service< Data::ExdDataGenerated >::ref();
|
2019-03-07 21:58:12 +11:00
|
|
|
|
2020-03-01 01:00:57 +11:00
|
|
|
auto actionData = exdData.get< Data::Action >( m_id );
|
2019-03-07 21:58:12 +11:00
|
|
|
assert( actionData );
|
|
|
|
|
|
|
|
m_actionData = actionData;
|
|
|
|
}
|
2019-03-07 20:27:45 +11:00
|
|
|
|
2019-07-26 20:28:01 +10:00
|
|
|
m_effectBuilder = make_EffectBuilder( m_pSource, getId(), m_sequence );
|
2019-07-25 22:46:10 +10:00
|
|
|
|
2019-03-08 00:26:41 +11:00
|
|
|
m_castTimeMs = static_cast< uint32_t >( m_actionData->cast100ms * 100 );
|
|
|
|
m_recastTimeMs = static_cast< uint32_t >( m_actionData->recast100ms * 100 );
|
2023-03-07 05:12:18 +09:00
|
|
|
auto actionCategory = static_cast< Common::ActionCategory >( m_actionData->actionCategory );
|
|
|
|
if( actionCategory == Common::ActionCategory::Spell || actionCategory == Common::ActionCategory::Weaponskill )
|
|
|
|
{
|
|
|
|
m_castTimeMs = static_cast< uint32_t >( m_castTimeMs * ( m_pSource->getStatValue( Common::BaseParam::Haste ) / 100.0f ) );
|
|
|
|
m_recastTimeMs = static_cast< uint32_t >( m_recastTimeMs * ( m_pSource->getStatValue( Common::BaseParam::Haste ) / 100.0f ) );
|
|
|
|
}
|
|
|
|
|
2019-03-07 21:58:12 +11:00
|
|
|
m_cooldownGroup = m_actionData->cooldownGroup;
|
|
|
|
m_range = m_actionData->range;
|
|
|
|
m_effectRange = m_actionData->effectRange;
|
2019-04-07 16:04:36 +10:00
|
|
|
m_castType = static_cast< Common::CastType >( m_actionData->castType );
|
2019-03-07 21:58:12 +11:00
|
|
|
m_aspect = static_cast< Common::ActionAspect >( m_actionData->aspect );
|
2019-02-11 01:50:41 +11:00
|
|
|
|
2019-04-07 16:04:36 +10:00
|
|
|
// todo: move this to bitset
|
|
|
|
m_canTargetSelf = m_actionData->canTargetSelf;
|
|
|
|
m_canTargetParty = m_actionData->canTargetParty;
|
|
|
|
m_canTargetFriendly = m_actionData->canTargetFriendly;
|
|
|
|
m_canTargetHostile = m_actionData->canTargetHostile;
|
|
|
|
// todo: this one doesn't look right based on whats in that col, probably has shifted
|
|
|
|
m_canTargetDead = m_actionData->canTargetDead;
|
|
|
|
|
2019-02-11 01:50:41 +11:00
|
|
|
// a default range is set by the game for the class/job
|
|
|
|
if( m_range == -1 )
|
|
|
|
{
|
2019-03-07 21:58:12 +11:00
|
|
|
switch( static_cast< Common::ClassJob >( m_actionData->classJob ) )
|
2019-02-11 01:50:41 +11:00
|
|
|
{
|
|
|
|
case Common::ClassJob::Bard:
|
|
|
|
case Common::ClassJob::Archer:
|
|
|
|
m_range = 25;
|
|
|
|
|
2019-03-07 21:58:12 +11:00
|
|
|
// anything that isnt ranged
|
2019-02-11 01:50:41 +11:00
|
|
|
default:
|
|
|
|
m_range = 3;
|
|
|
|
}
|
|
|
|
}
|
2019-02-09 18:01:39 +11:00
|
|
|
|
2019-06-15 20:56:19 +10:00
|
|
|
m_primaryCostType = static_cast< Common::ActionPrimaryCostType >( m_actionData->primaryCostType );
|
|
|
|
m_primaryCost = m_actionData->primaryCostValue;
|
2019-02-09 20:49:22 +11:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_primaryCostType != Common::ActionPrimaryCostType::None )
|
|
|
|
{
|
|
|
|
for( auto const& entry : m_pSource->getStatusEffectMap() )
|
|
|
|
{
|
|
|
|
if( entry.second->getParam() == 65436 ) // todo: decode this shit and figure out exact percentage to apply to primary cost, this magic number is 0%
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
Since the client is displaying correctly without additional data, there should be a "primary primary cost type" defined for each class.
|
|
|
|
In the case of 65436, on whm, mp cost is removed, on drk, blood cost is removed but mp cost remains.
|
|
|
|
*/
|
|
|
|
auto affectedPrimaryCost = Common::ActionPrimaryCostType::MagicPoints;
|
|
|
|
switch( m_pSource->getClass() )
|
|
|
|
{
|
|
|
|
case Common::ClassJob::Marauder:
|
|
|
|
case Common::ClassJob::Warrior:
|
|
|
|
{
|
|
|
|
affectedPrimaryCost = Common::ActionPrimaryCostType::WARGauge;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Common::ClassJob::Darkknight:
|
|
|
|
{
|
|
|
|
affectedPrimaryCost = Common::ActionPrimaryCostType::DRKGauge;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if( m_primaryCostType == affectedPrimaryCost )
|
|
|
|
{
|
|
|
|
setPrimaryCost( Common::ActionPrimaryCostType::None, 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( auto player = m_pSource->getAsPlayer() )
|
|
|
|
{
|
|
|
|
switch( player->getClass() )
|
|
|
|
{
|
|
|
|
case Common::ClassJob::Darkknight:
|
|
|
|
{
|
|
|
|
if( m_primaryCostType == Common::ActionPrimaryCostType::MagicPoints && player->gaugeDrkGetDarkArts() )
|
|
|
|
{
|
|
|
|
setPrimaryCost( Common::ActionPrimaryCostType::None, 0 );
|
|
|
|
player->gaugeDrkSetDarkArts( false );
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-28 23:34:43 +02:00
|
|
|
/*if( !m_actionData->targetArea )
|
2019-04-07 16:31:09 +10:00
|
|
|
{
|
|
|
|
// override pos to target position
|
|
|
|
// todo: this is kinda dirty
|
|
|
|
for( auto& actor : m_pSource->getInRangeActors() )
|
|
|
|
{
|
|
|
|
if( actor->getId() == m_targetId )
|
|
|
|
{
|
|
|
|
m_pos = actor->getPos();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2019-04-28 23:34:43 +02:00
|
|
|
}*/
|
2019-04-07 16:31:09 +10:00
|
|
|
|
2019-03-07 20:27:45 +11:00
|
|
|
// todo: add missing rows for secondaryCostType/secondaryCostType and rename the current rows to primaryCostX
|
2019-02-09 23:14:30 +11:00
|
|
|
|
2020-01-05 17:09:27 +09:00
|
|
|
if( ActionLut::validEntryExists( static_cast< uint16_t >( getId() ) ) )
|
|
|
|
{
|
|
|
|
m_lutEntry = ActionLut::getEntry( static_cast< uint16_t >( getId() ) );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::memset( &m_lutEntry, 0, sizeof( ActionEntry ) );
|
|
|
|
}
|
|
|
|
|
2019-04-07 16:04:36 +10:00
|
|
|
addDefaultActorFilters();
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
m_effectBuilder->setAnimationLock( getAnimationLock() );
|
|
|
|
|
2019-03-07 21:58:12 +11:00
|
|
|
return true;
|
2019-02-09 15:39:05 +11:00
|
|
|
}
|
2019-02-08 22:09:48 +11:00
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::setPos( Sapphire::Common::FFXIVARR_POSITION3 pos )
|
2019-02-09 18:32:10 +11:00
|
|
|
{
|
|
|
|
m_pos = pos;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
Sapphire::Common::FFXIVARR_POSITION3 Action::Action::getPos() const
|
2019-02-09 18:32:10 +11:00
|
|
|
{
|
|
|
|
return m_pos;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::setTargetId( uint64_t targetId )
|
2019-02-10 22:13:47 +11:00
|
|
|
{
|
|
|
|
m_targetId = targetId;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
uint64_t Action::Action::getTargetId() const
|
2019-02-10 22:13:47 +11:00
|
|
|
{
|
2019-03-07 21:58:12 +11:00
|
|
|
return m_targetId;
|
2019-02-10 22:13:47 +11:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::hasClientsideTarget() const
|
2017-08-08 13:53:47 +02:00
|
|
|
{
|
2019-03-07 21:58:12 +11:00
|
|
|
return m_targetId > 0xFFFFFFFF;
|
2017-08-08 13:53:47 +02:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::isInterrupted() const
|
2017-08-08 13:53:47 +02:00
|
|
|
{
|
2019-02-10 21:21:34 +11:00
|
|
|
return m_interruptType != Common::ActionInterruptType::None;
|
2017-08-08 13:53:47 +02:00
|
|
|
}
|
|
|
|
|
2020-01-05 17:09:27 +09:00
|
|
|
Common::ActionInterruptType Action::Action::getInterruptType() const
|
|
|
|
{
|
|
|
|
return m_interruptType;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
uint32_t Action::Action::getCastTime() const
|
2017-08-08 13:53:47 +02:00
|
|
|
{
|
2019-03-08 00:26:41 +11:00
|
|
|
return m_castTimeMs;
|
2017-08-08 13:53:47 +02:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::setCastTime( uint32_t castTime )
|
2017-08-08 13:53:47 +02:00
|
|
|
{
|
2019-03-08 00:26:41 +11:00
|
|
|
m_castTimeMs = castTime;
|
2017-08-08 13:53:47 +02:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::hasCastTime() const
|
2019-02-08 22:09:48 +11:00
|
|
|
{
|
2019-03-08 00:26:41 +11:00
|
|
|
return m_castTimeMs > 0;
|
2019-02-08 22:09:48 +11:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
Sapphire::Entity::CharaPtr Action::Action::getSourceChara() const
|
2017-08-08 13:53:47 +02:00
|
|
|
{
|
2018-08-29 21:40:59 +02:00
|
|
|
return m_pSource;
|
2017-08-08 13:53:47 +02:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::update()
|
2017-08-08 13:53:47 +02:00
|
|
|
{
|
2018-08-29 21:40:59 +02:00
|
|
|
// action has not been started yet
|
|
|
|
if( m_startTime == 0 )
|
|
|
|
return false;
|
|
|
|
|
2019-02-10 21:12:22 +11:00
|
|
|
if( isInterrupted() )
|
2018-08-29 21:40:59 +02:00
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-03-07 21:58:12 +11:00
|
|
|
if( !hasClientsideTarget() )
|
2019-02-10 22:13:47 +11:00
|
|
|
{
|
|
|
|
// todo: check if the target is still in range
|
|
|
|
}
|
|
|
|
|
2019-06-02 00:34:22 +10:00
|
|
|
uint64_t tickCount = Common::Util::getTimeMs();
|
2018-08-29 21:40:59 +02:00
|
|
|
|
2019-12-26 11:09:15 +03:00
|
|
|
if( !hasCastTime() || std::difftime( static_cast< time_t >( tickCount ), static_cast< time_t >( m_startTime ) ) > m_castTimeMs )
|
2018-08-29 21:40:59 +02:00
|
|
|
{
|
2019-03-07 20:33:14 +11:00
|
|
|
execute();
|
2018-08-29 21:40:59 +02:00
|
|
|
return true;
|
|
|
|
}
|
2019-02-10 21:01:04 +11:00
|
|
|
|
2020-01-05 17:09:27 +09:00
|
|
|
if( m_pTarget == nullptr && m_targetId != 0 )
|
|
|
|
{
|
|
|
|
// try to search for the target actor
|
|
|
|
for( auto actor : m_pSource->getInRangeActors( true ) )
|
|
|
|
{
|
|
|
|
if( actor->getId() == m_targetId )
|
|
|
|
{
|
|
|
|
m_pTarget = actor->getAsChara();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_pTarget != nullptr )
|
|
|
|
{
|
|
|
|
if( !m_pTarget->isAlive() )
|
|
|
|
{
|
|
|
|
// interrupt the cast if target died
|
|
|
|
interrupt();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-29 21:40:59 +02:00
|
|
|
return false;
|
2017-08-08 13:53:47 +02:00
|
|
|
}
|
2019-02-08 22:09:48 +11:00
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::start()
|
2019-02-08 22:09:48 +11:00
|
|
|
{
|
|
|
|
assert( m_pSource );
|
|
|
|
|
2019-06-02 00:34:22 +10:00
|
|
|
m_startTime = Common::Util::getTimeMs();
|
2019-02-11 00:40:00 +11:00
|
|
|
|
2019-02-10 22:13:47 +11:00
|
|
|
auto player = m_pSource->getAsPlayer();
|
|
|
|
|
2019-02-11 00:40:00 +11:00
|
|
|
if( hasCastTime() )
|
2019-02-08 22:09:48 +11:00
|
|
|
{
|
2019-07-29 22:22:45 +10:00
|
|
|
auto castPacket = makeZonePacket< Server::FFXIVIpcActorCast >( getId() );
|
2019-07-26 23:20:13 +10:00
|
|
|
auto& data = castPacket->data();
|
2019-02-08 22:09:48 +11:00
|
|
|
|
2019-07-26 23:20:13 +10:00
|
|
|
data.action_id = static_cast< uint16_t >( m_id );
|
|
|
|
data.skillType = Common::SkillType::Normal;
|
|
|
|
data.unknown_1 = m_id;
|
2019-02-08 22:09:48 +11:00
|
|
|
// This is used for the cast bar above the target bar of the caster.
|
2019-07-26 23:20:13 +10:00
|
|
|
data.cast_time = m_castTimeMs / 1000.f;
|
|
|
|
data.target_id = static_cast< uint32_t >( m_targetId );
|
|
|
|
|
|
|
|
auto pos = m_pSource->getPos();
|
|
|
|
data.posX = Common::Util::floatToUInt16( pos.x );
|
|
|
|
data.posY = Common::Util::floatToUInt16( pos.y );
|
|
|
|
data.posZ = Common::Util::floatToUInt16( pos.z );
|
2020-01-23 22:36:01 +09:00
|
|
|
data.rotation = Common::Util::floatToUInt16Rot( m_pSource->getRot() );
|
2019-02-08 22:09:48 +11:00
|
|
|
|
|
|
|
m_pSource->sendToInRangeSet( castPacket, true );
|
2019-02-10 21:18:03 +11:00
|
|
|
|
2019-02-10 22:13:47 +11:00
|
|
|
if( player )
|
2019-02-10 21:18:03 +11:00
|
|
|
{
|
|
|
|
player->setStateFlag( PlayerStateFlag::Casting );
|
|
|
|
}
|
2019-02-08 22:09:48 +11:00
|
|
|
}
|
2019-02-10 23:34:05 +11:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( player )
|
2019-06-02 02:30:54 +10:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
// todo: m_recastTimeMs needs to be adjusted for player sks/sps
|
|
|
|
auto actionStartPkt = makeActorControlSelf( m_pSource->getId(), ActorControlType::ActionStart, 1, getId(),
|
|
|
|
m_recastTimeMs / 10 );
|
|
|
|
player->queuePacket( actionStartPkt );
|
2019-02-11 00:40:00 +11:00
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
auto& scriptMgr = Common::Service< Scripting::ScriptMgr >::ref();
|
|
|
|
scriptMgr.onStart( *this );
|
|
|
|
|
2019-02-11 00:40:00 +11:00
|
|
|
// instantly finish cast if there's no cast time
|
|
|
|
if( !hasCastTime() )
|
2019-03-07 20:33:14 +11:00
|
|
|
execute();
|
2019-02-08 22:09:48 +11:00
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
void Action::Action::interrupt( ActionInterruptType type )
|
2019-02-08 22:09:48 +11:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
if( isInterrupted() )
|
|
|
|
return;
|
|
|
|
|
2019-02-08 22:09:48 +11:00
|
|
|
assert( m_pSource );
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
m_interruptType = type;
|
|
|
|
|
2019-02-10 21:01:04 +11:00
|
|
|
// things that aren't players don't care about cooldowns and state flags
|
|
|
|
if( m_pSource->isPlayer() )
|
2019-02-08 22:09:48 +11:00
|
|
|
{
|
2019-02-10 21:01:04 +11:00
|
|
|
auto player = m_pSource->getAsPlayer();
|
|
|
|
|
|
|
|
// todo: reset cooldown for actual player
|
|
|
|
|
|
|
|
// reset state flag
|
2019-02-10 21:18:03 +11:00
|
|
|
//player->unsetStateFlag( PlayerStateFlag::Occupied1 );
|
2019-02-10 21:01:04 +11:00
|
|
|
player->unsetStateFlag( PlayerStateFlag::Casting );
|
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_startTime > 0 && hasCastTime() )
|
2019-02-10 21:01:04 +11:00
|
|
|
{
|
2019-02-10 21:12:22 +11:00
|
|
|
uint8_t interruptEffect = 0;
|
|
|
|
if( m_interruptType == ActionInterruptType::DamageInterrupt )
|
|
|
|
interruptEffect = 1;
|
|
|
|
|
|
|
|
// Note: When cast interrupt from taking too much damage, set the last value to 1.
|
|
|
|
// This enables the cast interrupt effect.
|
2019-10-09 18:42:25 +02:00
|
|
|
auto control = makeActorControl( m_pSource->getId(), ActorControlType::CastInterrupt,
|
|
|
|
0x219, 1, m_id, interruptEffect );
|
2019-02-08 22:09:48 +11:00
|
|
|
|
|
|
|
m_pSource->sendToInRangeSet( control, true );
|
|
|
|
}
|
2019-02-10 23:34:05 +11:00
|
|
|
|
2020-03-01 01:00:57 +11:00
|
|
|
auto& scriptMgr = Common::Service< Scripting::ScriptMgr >::ref();
|
|
|
|
scriptMgr.onInterrupt( *this );
|
2019-02-08 22:09:48 +11:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::execute()
|
2019-02-08 22:09:48 +11:00
|
|
|
{
|
|
|
|
assert( m_pSource );
|
|
|
|
|
2019-04-04 21:56:59 +11:00
|
|
|
// subtract costs first, if somehow the caster stops meeting those requirements cancel the cast
|
2019-04-04 22:16:00 +11:00
|
|
|
if( !consumeResources() )
|
2019-04-04 21:56:59 +11:00
|
|
|
{
|
|
|
|
interrupt();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-01 01:00:57 +11:00
|
|
|
auto& scriptMgr = Common::Service< Scripting::ScriptMgr >::ref();
|
2019-02-08 22:09:48 +11:00
|
|
|
|
2019-02-11 00:40:00 +11:00
|
|
|
if( hasCastTime() )
|
2019-02-08 22:09:48 +11:00
|
|
|
{
|
2019-03-07 20:27:45 +11:00
|
|
|
// todo: what's this?
|
2019-10-09 18:14:53 +02:00
|
|
|
/*auto control = ActorControlSelfPacket( m_pTarget->getId(), ActorControlType::Unk7,
|
2019-02-08 22:09:48 +11:00
|
|
|
0x219, m_id, m_id, m_id, m_id );
|
|
|
|
m_pSource->sendToInRangeSet( control, true );*/
|
|
|
|
|
2019-03-07 22:41:05 +11:00
|
|
|
if( auto player = m_pSource->getAsPlayer() )
|
|
|
|
{
|
|
|
|
player->unsetStateFlag( PlayerStateFlag::Casting );
|
|
|
|
}
|
2019-02-10 22:13:47 +11:00
|
|
|
}
|
|
|
|
|
2020-01-05 17:09:27 +09:00
|
|
|
if( isCorrectCombo() )
|
2019-03-23 20:59:41 +11:00
|
|
|
{
|
|
|
|
auto player = m_pSource->getAsPlayer();
|
|
|
|
|
|
|
|
player->sendDebug( "action combo success from action#{0}", player->getLastComboActionId() );
|
|
|
|
}
|
|
|
|
|
2019-07-25 22:46:10 +10:00
|
|
|
if( !hasClientsideTarget() )
|
2019-02-10 22:13:47 +11:00
|
|
|
{
|
2019-07-25 22:46:10 +10:00
|
|
|
buildEffects();
|
2019-02-08 22:09:48 +11:00
|
|
|
}
|
2019-02-11 14:51:56 +11:00
|
|
|
else if( auto player = m_pSource->getAsPlayer() )
|
2019-02-10 22:13:47 +11:00
|
|
|
{
|
2020-03-01 01:00:57 +11:00
|
|
|
scriptMgr.onEObjHit( *player, m_targetId, getId() );
|
2019-02-10 22:13:47 +11:00
|
|
|
}
|
2019-03-23 20:59:41 +11:00
|
|
|
|
|
|
|
// set currently casted action as the combo action if it interrupts a combo
|
|
|
|
// ignore it otherwise (ogcds, etc.)
|
|
|
|
if( !m_actionData->preservesCombo )
|
|
|
|
{
|
2020-01-06 00:25:42 +09:00
|
|
|
// potential combo starter or correct combo from last action, must hit something to progress combo
|
|
|
|
if( !m_hitActors.empty() && ( !isComboAction() || isCorrectCombo() ) )
|
2020-01-05 17:09:27 +09:00
|
|
|
{
|
|
|
|
m_pSource->setLastComboActionId( getId() );
|
|
|
|
}
|
|
|
|
else // clear last combo action if the combo breaks
|
|
|
|
{
|
|
|
|
m_pSource->setLastComboActionId( 0 );
|
|
|
|
}
|
2019-03-23 20:59:41 +11:00
|
|
|
}
|
2019-02-08 22:09:48 +11:00
|
|
|
}
|
2019-02-09 17:36:44 +11:00
|
|
|
|
2019-07-27 00:37:40 +10:00
|
|
|
std::pair< uint32_t, Common::ActionHitSeverityType > Action::Action::calcDamage( uint32_t potency )
|
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_isAutoAttack )
|
|
|
|
return Math::CalcStats::calcAutoAttackDamage( *m_pSource, potency );
|
|
|
|
else
|
|
|
|
return Math::CalcStats::calcActionDamage( this, *m_pSource, potency, Math::CalcStats::getWeaponDamage( m_pSource ) );
|
2020-01-07 19:08:13 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
std::pair< uint32_t, Common::ActionHitSeverityType > Action::Action::calcHealing( uint32_t potency )
|
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
return Math::CalcStats::calcActionHealing( this, *m_pSource, potency, Math::CalcStats::getWeaponDamage( m_pSource ) );
|
2019-07-27 00:37:40 +10:00
|
|
|
}
|
|
|
|
|
2019-07-25 22:46:10 +10:00
|
|
|
void Action::Action::buildEffects()
|
|
|
|
{
|
|
|
|
snapshotAffectedActors( m_hitActors );
|
|
|
|
|
2020-03-01 01:00:57 +11:00
|
|
|
auto& scriptMgr = Common::Service< Scripting::ScriptMgr >::ref();
|
2019-07-25 22:46:10 +10:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
for( const auto& statusIt : m_pSource->getStatusEffectMap() )
|
2019-07-25 22:46:10 +10:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
statusIt.second->onActionExecute( this );
|
|
|
|
}
|
2019-07-25 22:46:10 +10:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
scriptMgr.onExecute( *this );
|
|
|
|
|
|
|
|
if( isInterrupted() )
|
2019-07-25 22:46:10 +10:00
|
|
|
return;
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_disableGenericHandler || !hasValidLutEntry() )
|
2020-01-06 19:25:01 +09:00
|
|
|
{
|
|
|
|
// send any effect packet added by script or an empty one just to play animation for other players
|
2023-03-07 05:12:18 +09:00
|
|
|
scriptMgr.onBeforeBuildEffect( *this, 0, 0 );
|
|
|
|
m_effectBuilder->buildAndSendPackets();
|
|
|
|
scriptMgr.onAfterBuildEffect( *this );
|
2019-07-25 22:46:10 +10:00
|
|
|
return;
|
2020-01-06 19:25:01 +09:00
|
|
|
}
|
2023-03-07 05:12:18 +09:00
|
|
|
|
|
|
|
// we have a valid lut entry
|
|
|
|
auto player = getSourceChara()->getAsPlayer();
|
|
|
|
if( player )
|
2019-07-25 22:46:10 +10:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
player->sendDebug( "type: {}, dpot: {} (dcpot: {}, ddpot: {}), hpot: {}, ss: {}, ts: {}, bonus: {}, breq: {}, bdata: {}",
|
|
|
|
m_actionData->attackType,
|
|
|
|
m_lutEntry.damagePotency, m_lutEntry.damageComboPotency, m_lutEntry.damageDirectionalPotency,
|
|
|
|
m_lutEntry.healPotency, m_lutEntry.selfStatus, m_lutEntry.targetStatus,
|
|
|
|
m_lutEntry.bonusEffect, m_lutEntry.bonusRequirement, m_lutEntry.getRawBonusData() );
|
2019-07-25 22:46:10 +10:00
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
uint8_t victimCounter = 0, validVictimCounter = 0;
|
2020-01-05 17:09:27 +09:00
|
|
|
|
2019-07-25 22:46:10 +10:00
|
|
|
for( auto& actor : m_hitActors )
|
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
victimCounter++;
|
|
|
|
bool shouldHitThisTarget = true;
|
|
|
|
for( const auto& statusIt : getSourceChara()->getStatusEffectMap() )
|
2019-07-27 00:37:40 +10:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
bool result = statusIt.second->onActionHitTarget( this, actor, victimCounter );
|
|
|
|
if( !result )
|
|
|
|
shouldHitThisTarget = false;
|
|
|
|
}
|
|
|
|
if( !shouldHitThisTarget )
|
|
|
|
continue;
|
|
|
|
if( m_lutEntry.damagePotency > 0 )
|
|
|
|
{
|
|
|
|
Common::AttackType attackType = static_cast< Common::AttackType >( m_actionData->attackType );
|
|
|
|
actor->onActionHostile( m_pSource );
|
|
|
|
|
|
|
|
auto dmg = calcDamage( isCorrectCombo() ? m_lutEntry.damageComboPotency : m_lutEntry.damagePotency );
|
|
|
|
if( victimCounter > 1 )
|
|
|
|
{
|
|
|
|
if( m_lutEntry.bonusEffect & Common::ActionBonusEffect::DamageFallOff )
|
|
|
|
{
|
|
|
|
if( checkActionBonusRequirement() )
|
|
|
|
{
|
|
|
|
dmg.first = static_cast< uint32_t >( 1.0 * dmg.first * ( m_lutEntry.getDamageFallOffPercentage() / 100.0 ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
dmg.first = Math::CalcStats::applyDamageReceiveMultiplier( *actor, dmg.first, getActionTypeFilterFromAttackType( attackType ) );
|
|
|
|
|
|
|
|
float originalDamage = dmg.first;
|
|
|
|
bool dodged = false;
|
|
|
|
float blocked = 0;
|
|
|
|
float parried = 0;
|
2020-01-05 17:09:27 +09:00
|
|
|
|
|
|
|
if( dmg.first > 0 )
|
2023-03-07 05:12:18 +09:00
|
|
|
{
|
|
|
|
dodged = Math::CalcStats::calcDodge( *actor );
|
2020-01-05 17:09:27 +09:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( !dodged && dmg.second == Common::ActionHitSeverityType::NormalDamage && actor->isPlayer() )
|
|
|
|
{
|
|
|
|
blocked = Math::CalcStats::calcBlock( *actor, dmg.first );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !dodged && blocked == 0 && dmg.second == Common::ActionHitSeverityType::NormalDamage && actor->isPlayer() )
|
|
|
|
{
|
|
|
|
if( isPhysical() )
|
|
|
|
{
|
|
|
|
parried = Math::CalcStats::calcParry( *actor, dmg.first );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( dodged )
|
|
|
|
dmg.first = 0;
|
|
|
|
else
|
2020-01-05 17:09:27 +09:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
dmg.first -= blocked;
|
|
|
|
dmg.first -= parried;
|
2020-01-05 17:09:27 +09:00
|
|
|
}
|
2019-07-27 00:37:40 +10:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( dmg.first > 0 )
|
2020-01-05 17:09:27 +09:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
dmg.first = actor->applyShieldProtection( dmg.first );
|
|
|
|
if( blocked > 0 )
|
|
|
|
m_effectBuilder->blockedDamage( actor, actor, dmg.first, static_cast< uint16_t >( blocked / originalDamage * 100 ) , dmg.first == 0 ? Common::ActionEffectResultFlag::Absorbed : Common::ActionEffectResultFlag::None, getExecutionDelay() + victimCounter * 100 );
|
|
|
|
else if (parried > 0 )
|
|
|
|
m_effectBuilder->parriedDamage( actor, actor, dmg.first, static_cast< uint16_t >( parried / originalDamage * 100 ), dmg.first == 0 ? Common::ActionEffectResultFlag::Absorbed : Common::ActionEffectResultFlag::None, getExecutionDelay() + victimCounter * 100 );
|
|
|
|
else
|
|
|
|
m_effectBuilder->damage( actor, actor, dmg.first, dmg.second, dmg.first == 0 ? Common::ActionEffectResultFlag::Absorbed : Common::ActionEffectResultFlag::None, getExecutionDelay() + victimCounter * 100 );
|
|
|
|
|
2023-03-08 02:47:31 +09:00
|
|
|
auto reflectDmg = Math::CalcStats::calcDamageReflect( m_pSource, actor, dmg.first, getActionTypeFilterFromAttackType( attackType ) );
|
2023-03-07 05:12:18 +09:00
|
|
|
if( reflectDmg.first > 0 )
|
2020-01-05 17:09:27 +09:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
m_effectBuilder->damage( actor, m_pSource, reflectDmg.first, reflectDmg.second, Common::ActionEffectResultFlag::Reflected, getExecutionDelay() + victimCounter * 100 );
|
2020-01-05 17:09:27 +09:00
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
auto absorb = Math::CalcStats::calcAbsorbHP( m_pSource, dmg.first );
|
|
|
|
if( absorb > 0 )
|
2020-01-05 17:09:27 +09:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
if( absorb > actor->getHp() )
|
|
|
|
absorb = actor->getHp();
|
|
|
|
m_effectBuilder->heal( actor, m_pSource, absorb, Common::ActionHitSeverityType::NormalHeal, Common::ActionEffectResultFlag::EffectOnSource, getExecutionDelay() + victimCounter * 100 );
|
2020-01-05 17:09:27 +09:00
|
|
|
}
|
2023-03-07 05:12:18 +09:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if( dodged )
|
2020-01-05 17:09:27 +09:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
m_effectBuilder->dodge( actor, actor );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// todo: no effect or invulnerable
|
2020-01-05 17:09:27 +09:00
|
|
|
}
|
|
|
|
}
|
2019-07-25 22:46:10 +10:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( !dodged )
|
2020-01-05 17:09:27 +09:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
if( ( !isComboAction() || isCorrectCombo() ) )
|
|
|
|
{
|
|
|
|
if ( !m_actionData->preservesCombo ) // this matches retail packet, on all standalone actions even casts.
|
|
|
|
{
|
|
|
|
m_effectBuilder->startCombo( actor, getId() ); // this is on all targets hit
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_lutEntry.bonusEffect & Common::ActionBonusEffect::SelfHeal )
|
|
|
|
{
|
|
|
|
if( checkActionBonusRequirement() )
|
|
|
|
{
|
|
|
|
auto heal = calcHealing( m_lutEntry.getSelfHealPotency() );
|
|
|
|
heal.first = Math::CalcStats::applyHealingReceiveMultiplier( *m_pSource, heal.first );
|
|
|
|
m_effectBuilder->heal( actor, m_pSource, heal.first, heal.second, Common::ActionEffectResultFlag::EffectOnSource );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-08 02:47:31 +09:00
|
|
|
if( validVictimCounter == 0 ) // effects only apply once if aoe, on first valid target (can be single target action as well)
|
2023-03-07 05:12:18 +09:00
|
|
|
{
|
|
|
|
if( isCorrectCombo() )
|
|
|
|
m_effectBuilder->comboSucceed( actor );
|
|
|
|
|
|
|
|
if( m_isAutoAttack && m_pSource->isPlayer() )
|
|
|
|
{
|
|
|
|
if( auto player = m_pSource->getAsPlayer() )
|
|
|
|
{
|
|
|
|
if( player->getClass() == Common::ClassJob::Paladin )
|
|
|
|
{
|
|
|
|
player->gaugePldSetOath( std::min( 100, player->gaugePldGetOath() + 5 ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_lutEntry.bonusEffect & Common::ActionBonusEffect::GainMPPercentage )
|
|
|
|
{
|
|
|
|
if( checkActionBonusRequirement() )
|
|
|
|
m_effectBuilder->restoreMP( actor, m_pSource, m_pSource->getMaxMp() * m_lutEntry.getMPGainPercentage() / 100, Common::ActionEffectResultFlag::EffectOnSource );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_lutEntry.bonusEffect & Common::ActionBonusEffect::GainJobResource )
|
|
|
|
{
|
|
|
|
if( checkActionBonusRequirement() )
|
|
|
|
{
|
|
|
|
switch( m_lutEntry.getAffectedJob() )
|
|
|
|
{
|
|
|
|
case Common::ClassJob::Marauder:
|
|
|
|
case Common::ClassJob::Warrior:
|
|
|
|
{
|
|
|
|
player->gaugeWarSetIb( std::min( 100, player->gaugeWarGetIb() + m_lutEntry.getJobResourceGain() ) );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Common::ClassJob::Darkknight:
|
|
|
|
{
|
|
|
|
player->gaugeDrkSetBlood( std::min( 100, player->gaugeDrkGetBlood() + m_lutEntry.getJobResourceGain() ) );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Common::ClassJob::Gunbreaker:
|
|
|
|
{
|
|
|
|
player->gaugeGnbSetAmmo( std::min( 2, player->gaugeGnbGetAmmo() + m_lutEntry.getJobResourceGain() ) );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Common::ClassJob::Samurai:
|
|
|
|
{
|
|
|
|
player->gaugeSamSetKenki( std::min( 100, player->gaugeSamGetKenki() + m_lutEntry.getJobResourceGain() ) );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_lutEntry.bonusEffect & Common::ActionBonusEffect::GainJobTimer )
|
|
|
|
{
|
|
|
|
if( checkActionBonusRequirement() )
|
|
|
|
{
|
|
|
|
switch( m_lutEntry.getAffectedJob() )
|
|
|
|
{
|
|
|
|
case Common::ClassJob::Darkknight:
|
|
|
|
{
|
|
|
|
player->gaugeDrkSetDarkSideTimer( std::min( 60000, player->gaugeDrkGetDarkSideTimer() + m_lutEntry.getJobTimerGain() ), true );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Common::ClassJob::Dragoon:
|
|
|
|
{
|
|
|
|
player->gaugeDrgSetDragonTimer( std::min( 30000, player->gaugeDrgGetDragonTimer() + m_lutEntry.getJobTimerGain() ), true );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
validVictimCounter++;
|
2020-01-05 17:09:27 +09:00
|
|
|
}
|
|
|
|
}
|
2023-03-07 05:12:18 +09:00
|
|
|
|
|
|
|
if( m_lutEntry.healPotency > 0 )
|
|
|
|
{
|
|
|
|
auto heal = calcHealing( m_lutEntry.healPotency );
|
|
|
|
heal.first = Math::CalcStats::applyHealingReceiveMultiplier( *actor, heal.first );
|
|
|
|
m_effectBuilder->heal( actor, actor, heal.first, heal.second, Common::ActionEffectResultFlag::None, getExecutionDelay() + victimCounter * 100 );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_lutEntry.targetStatus != 0 )
|
2019-07-27 00:37:40 +10:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
if( !isComboAction() || isCorrectCombo() )
|
|
|
|
m_effectBuilder->applyStatusEffect( actor, m_pSource, m_lutEntry.targetStatus, m_lutEntry.targetStatusDuration, m_lutEntry.targetStatusParam, getExecutionDelay() + victimCounter * 100 );
|
2019-07-27 00:37:40 +10:00
|
|
|
}
|
2019-07-25 22:46:10 +10:00
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_lutEntry.selfStatus != 0 )
|
|
|
|
{
|
|
|
|
if( !isComboAction() || isCorrectCombo() )
|
2023-03-08 02:47:31 +09:00
|
|
|
m_effectBuilder->applyStatusEffect( m_pSource, m_pSource, m_lutEntry.selfStatus, m_lutEntry.selfStatusDuration, m_lutEntry.selfStatusParam, getExecutionDelay() );
|
2023-03-07 05:12:18 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
scriptMgr.onBeforeBuildEffect( *this, victimCounter, validVictimCounter );
|
2019-07-25 22:46:10 +10:00
|
|
|
m_effectBuilder->buildAndSendPackets();
|
2023-03-07 05:12:18 +09:00
|
|
|
scriptMgr.onAfterBuildEffect( *this );
|
2019-07-25 22:46:10 +10:00
|
|
|
|
|
|
|
// at this point we're done with it and no longer need it
|
|
|
|
m_effectBuilder.reset();
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::preCheck()
|
2019-03-07 20:27:45 +11:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
auto& scriptMgr = Common::Service< Scripting::ScriptMgr >::ref();
|
|
|
|
scriptMgr.onBeforePreCheck( *this );
|
|
|
|
|
|
|
|
if( isInterrupted() )
|
|
|
|
return false;
|
|
|
|
|
2019-03-07 20:27:45 +11:00
|
|
|
if( auto player = m_pSource->getAsPlayer() )
|
|
|
|
{
|
2019-06-02 02:30:54 +10:00
|
|
|
if( !playerPreCheck( *player ) )
|
2019-03-07 20:27:45 +11:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::playerPreCheck( Entity::Player& player )
|
2019-03-07 20:27:45 +11:00
|
|
|
{
|
|
|
|
// lol
|
|
|
|
if( !player.isAlive() )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// npc actions/non player actions
|
2020-01-19 21:20:01 +09:00
|
|
|
if( m_actionData->classJob == -1 && !m_actionData->isRoleAction )
|
2019-03-07 20:27:45 +11:00
|
|
|
return false;
|
|
|
|
|
|
|
|
if( player.getLevel() < m_actionData->classJobLevel )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
auto currentClass = player.getClass();
|
|
|
|
auto actionClass = static_cast< Common::ClassJob >( m_actionData->classJob );
|
|
|
|
|
2020-01-19 21:20:01 +09:00
|
|
|
if( actionClass != Common::ClassJob::Adventurer && currentClass != actionClass && !m_actionData->isRoleAction )
|
2019-03-07 20:27:45 +11:00
|
|
|
{
|
|
|
|
// check if not a base class action
|
2020-03-01 01:00:57 +11:00
|
|
|
auto& exdData = Common::Service< Data::ExdDataGenerated >::ref();
|
2019-03-07 20:27:45 +11:00
|
|
|
|
2020-03-01 01:00:57 +11:00
|
|
|
auto classJob = exdData.get< Data::ClassJob >( static_cast< uint8_t >( currentClass ) );
|
2019-03-07 20:27:45 +11:00
|
|
|
if( !classJob )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( classJob->classJobParent != m_actionData->classJob )
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-06-02 02:30:54 +10:00
|
|
|
if( !m_actionData->canTargetSelf && getTargetId() == m_pSource->getId() )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// todo: does this need to check for party/alliance stuff or it's just same type?
|
|
|
|
// todo: m_pTarget doesn't exist at this stage because we only fill it when we snapshot targets
|
|
|
|
// if( !m_actionData->canTargetFriendly && m_pSource->getObjKind() == m_pTarget->getObjKind() )
|
|
|
|
// return false;
|
|
|
|
//
|
|
|
|
// if( !m_actionData->canTargetHostile && m_pSource->getObjKind() != m_pTarget->getObjKind() )
|
|
|
|
// return false;
|
|
|
|
|
|
|
|
// todo: party/dead validation
|
2019-03-07 20:27:45 +11:00
|
|
|
|
|
|
|
// validate range
|
|
|
|
|
|
|
|
|
2019-04-04 22:16:00 +11:00
|
|
|
if( !hasResources() )
|
2019-04-04 21:56:59 +11:00
|
|
|
return false;
|
2019-03-07 21:58:12 +11:00
|
|
|
|
2019-03-07 20:27:45 +11:00
|
|
|
return true;
|
2019-03-07 21:58:12 +11:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
uint32_t Action::Action::getAdditionalData() const
|
2019-03-07 21:58:12 +11:00
|
|
|
{
|
|
|
|
return m_additionalData;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::setAdditionalData( uint32_t data )
|
2019-03-07 21:58:12 +11:00
|
|
|
{
|
|
|
|
m_additionalData = data;
|
2019-03-23 20:59:41 +11:00
|
|
|
}
|
|
|
|
|
2020-01-05 17:09:27 +09:00
|
|
|
bool Action::Action::isCorrectCombo() const
|
2019-03-23 20:59:41 +11:00
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_shouldAlwaysCombo )
|
|
|
|
return true;
|
|
|
|
|
2019-03-23 20:59:41 +11:00
|
|
|
auto lastActionId = m_pSource->getLastComboActionId();
|
|
|
|
|
|
|
|
if( lastActionId == 0 )
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return m_actionData->actionCombo == lastActionId;
|
2019-04-04 21:56:59 +11:00
|
|
|
}
|
|
|
|
|
2020-01-05 17:09:27 +09:00
|
|
|
bool Action::Action::isComboAction() const
|
|
|
|
{
|
|
|
|
return m_actionData->actionCombo != 0;
|
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
void Sapphire::World::Action::Action::setAlwaysCombo()
|
|
|
|
{
|
|
|
|
m_shouldAlwaysCombo = true;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::primaryCostCheck( bool subtractCosts )
|
2019-04-04 21:56:59 +11:00
|
|
|
{
|
|
|
|
switch( m_primaryCostType )
|
|
|
|
{
|
|
|
|
case Common::ActionPrimaryCostType::TacticsPoints:
|
|
|
|
{
|
|
|
|
auto curTp = m_pSource->getTp();
|
|
|
|
|
|
|
|
if( curTp < m_primaryCost )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( subtractCosts )
|
|
|
|
m_pSource->setTp( curTp - m_primaryCost );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::MagicPoints:
|
|
|
|
{
|
|
|
|
auto curMp = m_pSource->getMp();
|
|
|
|
|
2019-06-30 19:51:18 +10:00
|
|
|
auto cost = m_primaryCost * 100;
|
2019-04-04 21:56:59 +11:00
|
|
|
|
2019-12-26 11:09:15 +03:00
|
|
|
if( curMp < static_cast< uint32_t >( cost ) )
|
2019-04-04 21:56:59 +11:00
|
|
|
return false;
|
|
|
|
|
|
|
|
if( subtractCosts )
|
2019-12-26 11:09:15 +03:00
|
|
|
m_pSource->setMp( curMp - static_cast< uint32_t >( cost ) );
|
2019-04-04 21:56:59 +11:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
case Common::ActionPrimaryCostType::StatusEffect:
|
|
|
|
{
|
|
|
|
auto statusEntry = m_pSource->getStatusEffectById( m_primaryCost );
|
|
|
|
|
|
|
|
if( !statusEntry.second )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( subtractCosts )
|
|
|
|
m_pSource->removeStatusEffect( statusEntry.first );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::WARGauge:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
auto ib = pPlayer->gaugeWarGetIb();
|
|
|
|
if( ib >= m_primaryCost )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
pPlayer->gaugeWarSetIb( ib - m_primaryCost );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::PLDGauge:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
auto oath = pPlayer->gaugePldGetOath();
|
|
|
|
if( oath >= m_primaryCost )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
pPlayer->gaugePldSetOath( oath - m_primaryCost );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::WHMBloodLily:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
auto bloodLily = pPlayer->gaugeWhmGetBloodLily();
|
|
|
|
if( bloodLily >= m_primaryCost )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
pPlayer->gaugeWhmSetLilies( pPlayer->gaugeWhmGetLily(), bloodLily - m_primaryCost );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::WHMLily:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
auto lily = pPlayer->gaugeWhmGetLily();
|
|
|
|
if( lily >= m_primaryCost )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
{
|
|
|
|
lily -= m_primaryCost;
|
|
|
|
auto bloodLily = pPlayer->gaugeWhmGetBloodLily();
|
|
|
|
if( pPlayer->getLevel() >= 74 )
|
|
|
|
{
|
|
|
|
bloodLily = std::min( 3, bloodLily + m_primaryCost );
|
|
|
|
}
|
|
|
|
pPlayer->gaugeWhmSetLilies( lily, bloodLily );
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::DRKGauge:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
auto blood = pPlayer->gaugeDrkGetBlood();
|
|
|
|
if( blood >= m_primaryCost )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
pPlayer->gaugeDrkSetBlood( blood - m_primaryCost );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::GNBAmmo:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
auto ammo = pPlayer->gaugeGnbGetAmmo();
|
|
|
|
if( ammo >= m_primaryCost )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
pPlayer->gaugeGnbSetAmmo( ammo - m_primaryCost );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::SAMKenki:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
auto kenki = pPlayer->gaugeSamGetKenki();
|
|
|
|
if( kenki >= m_primaryCost )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
pPlayer->gaugeSamSetKenki( kenki - m_primaryCost );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::SAMSen:
|
|
|
|
{
|
|
|
|
auto pPlayer = m_pSource->getAsPlayer();
|
|
|
|
if( pPlayer )
|
|
|
|
{
|
|
|
|
// trust the client and assume the action is correct, you can cheat this by performing one sen midare :)
|
|
|
|
if( pPlayer->gaugeSamHasAnySen() )
|
|
|
|
{
|
|
|
|
if( subtractCosts )
|
|
|
|
pPlayer->gaugeSamSetSen( Common::SamSen::None );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::ActionPrimaryCostType::SAMMeditation:
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-04-04 22:00:36 +11:00
|
|
|
// free casts, likely just pure ogcds
|
2019-04-04 21:56:59 +11:00
|
|
|
case Common::ActionPrimaryCostType::None:
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::secondaryCostCheck( bool subtractCosts )
|
2019-04-04 21:56:59 +11:00
|
|
|
{
|
|
|
|
// todo: these need to be mapped
|
|
|
|
return true;
|
2019-04-04 22:16:00 +11:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::hasResources()
|
2019-04-04 22:16:00 +11:00
|
|
|
{
|
|
|
|
return primaryCostCheck( false ) && secondaryCostCheck( false );
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::consumeResources()
|
2019-04-04 22:16:00 +11:00
|
|
|
{
|
|
|
|
return primaryCostCheck( true ) && secondaryCostCheck( true );
|
2019-04-07 16:04:36 +10:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::snapshotAffectedActors( std::vector< Entity::CharaPtr >& actors )
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
2019-05-28 22:03:55 +10:00
|
|
|
for( const auto& actor : m_pSource->getInRangeActors( true ) )
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
|
|
|
// check for initial target validity based on flags in action exd (pc/enemy/etc.)
|
|
|
|
if( !preFilterActor( *actor ) )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
for( const auto& filter : m_actorFilters )
|
|
|
|
{
|
|
|
|
if( filter->conditionApplies( *actor ) )
|
|
|
|
{
|
|
|
|
actors.push_back( actor->getAsChara() );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( auto player = m_pSource->getAsPlayer() )
|
|
|
|
{
|
|
|
|
player->sendDebug( "Hit {} actors with {} filters", actors.size(), m_actorFilters.size() );
|
|
|
|
for( const auto& actor : actors )
|
|
|
|
{
|
|
|
|
player->sendDebug( "hit actor#{}", actor->getId() );
|
|
|
|
}
|
|
|
|
}
|
2019-05-28 22:03:55 +10:00
|
|
|
|
|
|
|
return !actors.empty();
|
2019-04-07 16:04:36 +10:00
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::addActorFilter( World::Util::ActorFilterPtr filter )
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
|
|
|
m_actorFilters.push_back( std::move( filter ) );
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
void Action::Action::addDefaultActorFilters()
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
|
|
|
switch( m_castType )
|
|
|
|
{
|
|
|
|
case Common::CastType::SingleTarget:
|
2020-01-23 22:36:01 +09:00
|
|
|
case Common::CastType::Type3:
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
|
|
|
auto filter = std::make_shared< World::Util::ActorFilterSingleTarget >( static_cast< uint32_t >( m_targetId ) );
|
|
|
|
|
|
|
|
addActorFilter( filter );
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case Common::CastType::CircularAOE:
|
|
|
|
{
|
|
|
|
auto filter = std::make_shared< World::Util::ActorFilterInRange >( m_pos, m_effectRange );
|
|
|
|
|
|
|
|
addActorFilter( filter );
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
case Common::CastType::RectangularAOE:
|
|
|
|
{
|
|
|
|
auto filter = std::make_shared< World::Util::ActorFilterInRange >( m_pos, m_effectRange );
|
|
|
|
addActorFilter( filter );
|
|
|
|
break;
|
|
|
|
}
|
2019-04-07 16:04:36 +10:00
|
|
|
|
|
|
|
default:
|
|
|
|
{
|
|
|
|
Logger::error( "[{}] Action#{} has CastType#{} but that cast type is unhandled. Cancelling cast.",
|
|
|
|
m_pSource->getId(), getId(), m_castType );
|
|
|
|
|
|
|
|
interrupt();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
bool Action::Action::preFilterActor( Sapphire::Entity::Actor& actor ) const
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
|
|
|
auto kind = actor.getObjKind();
|
2020-01-05 17:09:27 +09:00
|
|
|
auto chara = actor.getAsChara();
|
2019-04-07 16:04:36 +10:00
|
|
|
|
|
|
|
// todo: are there any server side eobjs that players can hit?
|
|
|
|
if( kind != ObjKind::BattleNpc && kind != ObjKind::Player )
|
|
|
|
return false;
|
2020-01-06 04:29:45 +09:00
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_lutEntry.damagePotency > 0 && chara->getId() == m_pSource->getId() ) // !m_canTargetSelf
|
2020-01-06 04:29:45 +09:00
|
|
|
return false;
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( ( m_lutEntry.damagePotency > 0 || m_lutEntry.healPotency > 0 ) && !chara->isAlive() ) // !m_canTargetDead not working for aoe
|
2020-01-05 17:09:27 +09:00
|
|
|
return false;
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( m_lutEntry.damagePotency > 0 && m_pSource->getObjKind() == chara->getObjKind() ) // !m_canTargetFriendly not working for aoe
|
2020-01-05 17:09:27 +09:00
|
|
|
return false;
|
|
|
|
|
2023-03-07 05:12:18 +09:00
|
|
|
if( ( m_lutEntry.damagePotency == 0 && m_lutEntry.healPotency > 0 ) && m_pSource->getObjKind() != chara->getObjKind() ) // !m_canTargetHostile not working for aoe
|
2020-01-06 04:29:45 +09:00
|
|
|
return false;
|
2023-03-07 05:12:18 +09:00
|
|
|
|
2019-04-07 16:04:36 +10:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
std::vector< Sapphire::Entity::CharaPtr >& Action::Action::getHitCharas()
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
|
|
|
return m_hitActors;
|
|
|
|
}
|
|
|
|
|
2019-06-03 00:39:02 +10:00
|
|
|
Sapphire::Entity::CharaPtr Action::Action::getHitChara()
|
2019-04-07 16:04:36 +10:00
|
|
|
{
|
|
|
|
if( !m_hitActors.empty() )
|
|
|
|
{
|
|
|
|
return m_hitActors.at( 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
return nullptr;
|
2020-01-05 17:09:27 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::hasValidLutEntry() const
|
|
|
|
{
|
2023-03-07 05:12:18 +09:00
|
|
|
return m_lutEntry.damagePotency != 0 || m_lutEntry.healPotency != 0 || m_lutEntry.selfStatus != 0 ||
|
|
|
|
m_lutEntry.targetStatus != 0 || m_lutEntry.bonusEffect != 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
float Action::Action::getAnimationLock()
|
|
|
|
{
|
|
|
|
switch( static_cast< Common::ActionCategory >( m_actionData->actionCategory ) )
|
|
|
|
{
|
|
|
|
case Common::ActionCategory::Item:
|
|
|
|
{
|
|
|
|
return 1.1f;
|
|
|
|
}
|
|
|
|
case Common::ActionCategory::Mount:
|
|
|
|
{
|
|
|
|
return 0.1f;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return hasCastTime() ? 0.1f : 0.6f;
|
2020-01-06 19:25:01 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
Action::EffectBuilderPtr Action::Action::getEffectbuilder()
|
|
|
|
{
|
|
|
|
return m_effectBuilder;
|
2023-03-07 05:12:18 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
Data::ActionPtr Action::Action::getActionData()
|
|
|
|
{
|
|
|
|
return m_actionData;
|
|
|
|
}
|
|
|
|
|
|
|
|
Action::ActionEntry& Action::Action::getActionEntry()
|
|
|
|
{
|
|
|
|
return m_lutEntry;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Action::Action::setAutoAttack()
|
|
|
|
{
|
|
|
|
m_isAutoAttack = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Action::Action::disableGenericHandler()
|
|
|
|
{
|
|
|
|
m_disableGenericHandler = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::isPhysical() const
|
|
|
|
{
|
|
|
|
return isAttackTypePhysical( static_cast< Common::AttackType >( m_actionData->attackType ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::isMagical() const
|
|
|
|
{
|
|
|
|
return isAttackTypeMagical( static_cast< Common::AttackType >( m_actionData->attackType ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::isGCD() const
|
|
|
|
{
|
|
|
|
auto actionCategory = static_cast< Common::ActionCategory >( m_actionData->actionCategory );
|
|
|
|
return actionCategory == Common::ActionCategory::Weaponskill || actionCategory == Common::ActionCategory::Spell;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::isWeaponSkill() const
|
|
|
|
{
|
|
|
|
auto actionCategory = static_cast< Common::ActionCategory >( m_actionData->actionCategory );
|
|
|
|
return actionCategory == Common::ActionCategory::Weaponskill;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::isAttackTypePhysical( Common::AttackType attackType )
|
|
|
|
{
|
|
|
|
return attackType == Common::AttackType::Physical;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::isAttackTypeMagical( Common::AttackType attackType )
|
|
|
|
{
|
|
|
|
return attackType == Common::AttackType::Magical;
|
|
|
|
}
|
|
|
|
|
|
|
|
ActionTypeFilter Sapphire::World::Action::Action::getActionTypeFilterFromAttackType( AttackType attackType )
|
|
|
|
{
|
|
|
|
switch( attackType )
|
|
|
|
{
|
|
|
|
case AttackType::Physical:
|
|
|
|
return ActionTypeFilter::Physical;
|
|
|
|
case AttackType::Magical:
|
|
|
|
return ActionTypeFilter::Magical;
|
|
|
|
case AttackType::Slashing:
|
|
|
|
return static_cast< ActionTypeFilter >( static_cast< uint32_t >( ActionTypeFilter::Slashing ) + static_cast< uint32_t >( ActionTypeFilter::Physical ) );
|
|
|
|
case AttackType::Piercing:
|
|
|
|
return static_cast< ActionTypeFilter >( static_cast< uint32_t >( ActionTypeFilter::Piercing ) + static_cast< uint32_t >( ActionTypeFilter::Physical ) );
|
|
|
|
case AttackType::Blunt:
|
|
|
|
return static_cast< ActionTypeFilter >( static_cast< uint32_t >( ActionTypeFilter::Blunt ) + static_cast< uint32_t >( ActionTypeFilter::Physical ) );
|
|
|
|
default:
|
|
|
|
return ActionTypeFilter::Unknown;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Action::Action::setPrimaryCost( Common::ActionPrimaryCostType type, uint16_t cost )
|
|
|
|
{
|
|
|
|
m_primaryCostType = type;
|
|
|
|
m_primaryCost = cost;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Action::Action::checkActionBonusRequirement()
|
|
|
|
{
|
|
|
|
if( !m_pSource->isPlayer() )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if( m_lutEntry.bonusRequirement & Common::ActionBonusEffectRequirement::RequireCorrectCombo )
|
|
|
|
{
|
|
|
|
if( !isCorrectCombo() )
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if( m_lutEntry.bonusRequirement & Common::ActionBonusEffectRequirement::RequireCorrectPositional )
|
|
|
|
{
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint64_t Action::Action::getExecutionDelay() const
|
|
|
|
{
|
|
|
|
// let's see how 3.x is going to do it
|
|
|
|
return 600;
|
2019-02-09 23:14:30 +11:00
|
|
|
}
|