diff --git a/src/world/AI/TargetHelper.cpp b/src/world/AI/TargetHelper.cpp new file mode 100644 index 00000000..4674d7de --- /dev/null +++ b/src/world/AI/TargetHelper.cpp @@ -0,0 +1,218 @@ +#include "TargetHelper.h" + +#include + +#include +#include +#include +#include +#include + +namespace Sapphire::World::AI +{ + + bool InsideRadiusFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + bool ret = Common::Util::distance( pSrc->getPos(), pTarget->getPos() ) <= m_distance; + return m_negate ? !ret : ret; + } + + bool OutsideRadiusFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + bool ret = Common::Util::distance( pSrc->getPos(), pTarget->getPos() ) >= m_distance; + return m_negate ? !ret : ret; + } + + bool PlayerFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + bool ret = pTarget->isPlayer(); + return m_negate ? !ret : ret; + } + + bool AllyFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + bool ret = false; + if( pSrc->isPlayer() ) + { + auto pBNpcTarget = pTarget->getAsBNpc(); + if( pBNpcTarget && pBNpcTarget->getEnemyType() == 0 ) + ret = true; + else if( pTarget->isPlayer() ) + ret = true; + } + else if( pSrc->isBattleNpc() ) + { + auto pBNpcTarget = pTarget->getAsBNpc(); + if( pBNpcTarget && pBNpcTarget->getEnemyType() == 0 ) + ret = true; + else if( pTarget->isPlayer() ) + ret = true; + } + + return m_negate ? !ret : ret; + } + + bool OwnBattalionFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + return false; + } + + bool TankFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + bool ret = pTarget->getRole() == Common::Role::Tank; + return m_negate ? !ret : ret; + } + + bool HealerFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + bool ret = pTarget->getRole() == Common::Role::Healer; + return m_negate ? !ret : ret; + } + + bool DpsFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + bool ret = true; + switch( pTarget->getRole() ) + { + case Common::Role::Melee: + case Common::Role::RangedMagical: + case Common::Role::RangedPhysical: + ret = true; + break; + default: + ret = false; + break; + } + return m_negate ? !ret : ret; + } + + bool HasStatusEffectFilter::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + auto ret = pTarget->hasStatusEffect( m_statusId ); + return m_negate ? !ret : ret; + } + + bool TopAggroFilter ::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + auto pBNpc = pSrc->getAsBNpc(); + bool ret = false; + if( pBNpc ) + { + ret = pBNpc->hateListGetHighest() == pTarget; + } + return m_negate ? !ret : ret; + } + + bool SecondAggroFilter ::isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + auto pBNpc = pSrc->getAsBNpc(); + bool ret = false; + if( pBNpc ) + { + // todo: this is so dumb + + auto hateList = pBNpc->getHateList(); + std::vector sorted( hateList.begin(), hateList.end() ); + std::sort( sorted.begin(), sorted.end(), []( Entity::HateListEntryPtr a, Entity::HateListEntryPtr b ) { + return a->m_hateAmount > b->m_hateAmount; } ); + + Entity::CharaPtr pChara = nullptr; + auto topIt = sorted.begin(); + if( topIt != sorted.end() && ++topIt != sorted.end() ) + pChara = topIt->get()->m_pChara; + + ret = pChara == pTarget; + } + return m_negate ? !ret : ret; + } + + void Snapshot::createSnapshot( Entity::CharaPtr pSrc, const std::set< Entity::GameObjectPtr >& inRange, + int count, bool fillWithRandom, const std::set< uint32_t >& exclude ) + { + m_targets.clear(); + + auto& RNGMgr = Common::Service< World::Manager::RNGMgr >::ref(); + for( const auto& pActor : inRange ) + { + auto pChara = pActor->getAsChara(); + if( pChara == nullptr ) + continue; + + if( exclude.find( pChara->getId() ) != exclude.end() ) continue; + + bool matches = true; + for( const auto& filter : m_filters ) + { + if( !filter->isConditionMet( pSrc, pChara ) ) + { + matches = false; + break; + } + } + + if( matches ) + { + CharaEntry entry; + entry.m_entityId = pChara->getId(); + entry.m_pos = pChara->getPos(); + entry.m_rot = pChara->getRot(); + + m_targets.push_back( entry ); + if( m_targets.size() == count ) break; + } + } + + if( fillWithRandom && m_targets.size() < count ) + { + std::vector< Entity::CharaPtr > remaining; + for( const auto& pActor : inRange ) + { + auto pChara = pActor->getAsChara(); + if( pChara == nullptr ) + continue; + + if( exclude.find( pChara->getId() ) == exclude.end() && std::find_if( m_targets.begin(), m_targets.end(), + [ &pChara ]( CharaEntry entry ) { return entry.m_entityId == pChara->getId(); } ) == m_targets.end() ) + { + remaining.push_back( pChara ); + } + } + while( m_targets.size() < count && !remaining.empty() ) + { + // idk + std::shuffle( remaining.begin(), remaining.end(), *RNGMgr.getRNGEngine().get() ); + + auto pChara = remaining.back(); + CharaEntry entry; + entry.m_entityId = pChara->getId(); + entry.m_pos = pChara->getPos(); + entry.m_rot = pChara->getRot(); + m_targets.emplace_back( entry ); + remaining.pop_back(); + } + } + + // sort by distance at the end always + auto srcPos = pSrc->getPos(); + std::sort( m_targets.begin(), m_targets.end(), + [ srcPos ]( CharaEntry l, CharaEntry r ) + { + return Common::Util::distance( srcPos, l.m_pos ) < Common::Util::distance( srcPos, r.m_pos ); + } + ); + } + + const std::vector< Snapshot::CharaEntry >& Snapshot::getResults() const + { + return m_targets; + } + + const std::vector< uint32_t > Snapshot::getTargetIds() const + { + std::vector< uint32_t > ret( m_targets.size() ); + for( auto i = 0; i < m_targets.size(); ++i ) + ret[ i ] = m_targets[ i ].m_entityId; + return ret; + } + +};// namespace Sapphire::World::AI \ No newline at end of file diff --git a/src/world/AI/TargetHelper.h b/src/world/AI/TargetHelper.h new file mode 100644 index 00000000..71ba2ffb --- /dev/null +++ b/src/world/AI/TargetHelper.h @@ -0,0 +1,232 @@ +#ifndef _TARGETHELPER_H +#define _TARGETHELPER_H + +#include +#include +#include +#include + +#include + +namespace Sapphire::World::AI +{ + // + // Filters + // + class TargetSelectFilter : + public std::enable_shared_from_this< TargetSelectFilter > + { + public: + enum class Type + { + InsideRadius, + OutsideRadius, + + Player, + Ally, + OwnBattalion, + + Tank, + Healer, + Dps, + + HasStatusEffect, + + TopAggro, + SecondAggro, + + AllianceA, + AllianceB, + AllianceC + }; + + protected: + Type m_type; + bool m_negate{ false }; + + public: + TargetSelectFilter( Type type, bool negate ) : + m_type( type ), + m_negate( negate ) + { + } + virtual ~TargetSelectFilter() + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const + { + return false; + }; + + bool isNegate() const + { + return m_negate; + } + }; + using TargetSelectFilterPtr = std::shared_ptr< TargetSelectFilter >; + + class InsideRadiusFilter : public TargetSelectFilter + { + private: + float m_distance{ 0 }; + + public: + InsideRadiusFilter( float distance, bool negate ) : + TargetSelectFilter( Type::InsideRadius, negate ), + m_distance( distance ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class OutsideRadiusFilter : public TargetSelectFilter + { + private: + float m_distance{ 0 }; + public: + OutsideRadiusFilter( float distance, bool negate ) : + TargetSelectFilter( Type::OutsideRadius, negate ), + m_distance( distance ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class PlayerFilter : public TargetSelectFilter + { + public: + PlayerFilter( bool negate ) : + TargetSelectFilter( Type::Player, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class AllyFilter : public TargetSelectFilter + { + public: + AllyFilter( bool negate ) : + TargetSelectFilter( Type::Ally, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class OwnBattalionFilter : public TargetSelectFilter + { + public: + OwnBattalionFilter( bool negate ) : + TargetSelectFilter( Type::OwnBattalion, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class TankFilter : public TargetSelectFilter + { + public: + TankFilter( bool negate ) : + TargetSelectFilter( Type::Tank, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class HealerFilter : public TargetSelectFilter + { + public: + HealerFilter( bool negate ) : + TargetSelectFilter( Type::Healer, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class DpsFilter : public TargetSelectFilter + { + public: + DpsFilter( bool negate ) : + TargetSelectFilter( Type::Dps, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class HasStatusEffectFilter : public TargetSelectFilter + { + private: + uint32_t m_statusId{ 0 }; + public: + HasStatusEffectFilter( uint32_t statusId, bool negate ) : + TargetSelectFilter( Type::HasStatusEffect, negate ), + m_statusId( statusId ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class TopAggroFilter : public TargetSelectFilter + { + public: + TopAggroFilter( bool negate ) : + TargetSelectFilter( Type::TopAggro, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + class SecondAggroFilter : public TargetSelectFilter + { + public: + SecondAggroFilter( bool negate ) : + TargetSelectFilter( Type::SecondAggro, negate ) + { + } + + bool isConditionMet( Entity::CharaPtr& pSrc, Entity::CharaPtr& pTarget ) const; + }; + + // + // Helpers + // + class Snapshot : + public std::enable_shared_from_this< Snapshot > + { + struct CharaEntry + { + uint32_t m_entityId; + Common::FFXIVARR_POSITION3 m_pos; + float m_rot; + // todo: status effects? + }; + private: + std::vector< TargetSelectFilterPtr > m_filters; + std::vector< CharaEntry > m_targets; + +public: + Snapshot( const std::vector< TargetSelectFilterPtr > filters ) : + m_filters( filters ) + { + } + void createSnapshot( Entity::CharaPtr pSrc, const std::set< Entity::GameObjectPtr >& inRange, + int count, bool fillWithRandom, const std::set< uint32_t >& exclude = {} ); + + // returns actors sorted by distance + const std::vector< CharaEntry >& getResults() const; + const std::vector< uint32_t > getTargetIds() const; + }; + using SnapshotPtr = std::shared_ptr< Snapshot >; +}// namespace Sapphire::World::AI + +#endif \ No newline at end of file diff --git a/src/world/Actor/BNpc.cpp b/src/world/Actor/BNpc.cpp index fa349899..849b16c6 100644 --- a/src/world/Actor/BNpc.cpp +++ b/src/world/Actor/BNpc.cpp @@ -442,6 +442,11 @@ void BNpc::sendPositionUpdate() server().queueForPlayers( getInRangePlayerIds(), movePacket ); } +const std::set< std::shared_ptr< HateListEntry > >& BNpc::getHateList() const +{ + return m_hateList; +} + void BNpc::hateListClear() { for( auto& listEntry : m_hateList ) @@ -825,6 +830,11 @@ void BNpc::setFlag( uint32_t flag ) m_flags |= flag; } +void BNpc::removeFlag( uint32_t flag ) +{ + m_flags &= ~flag; +} + void BNpc::clearFlags() { m_flags = 0; diff --git a/src/world/Actor/BNpc.h b/src/world/Actor/BNpc.h index 307df62b..86044b92 100644 --- a/src/world/Actor/BNpc.h +++ b/src/world/Actor/BNpc.h @@ -18,6 +18,7 @@ namespace Sapphire::Entity uint32_t m_hateAmount; CharaPtr m_pChara; }; + using HateListEntryPtr = std::shared_ptr< HateListEntry >; enum class BNpcState { @@ -39,6 +40,7 @@ namespace Sapphire::Entity NoDeaggro = 0x10, Untargetable = 0x20, AutoAttackDisabled = 0x40, + Invisible = 0x80, Intermission = 0x77 // for transition phases to ensure boss only moves/acts when scripted }; @@ -106,6 +108,7 @@ namespace Sapphire::Entity BNpcState getState() const; void setState( BNpcState state ); + const std::set< std::shared_ptr< HateListEntry > >& getHateList() const; void hateListClear(); uint32_t hateListGetValue( const Sapphire::Entity::CharaPtr& pChara ); uint32_t hateListGetHighestValue(); @@ -115,7 +118,7 @@ namespace Sapphire::Entity void hateListUpdate( const CharaPtr& pChara, int32_t hateAmount ); void hateListRemove( const CharaPtr& pChara ); bool hateListHasActor( const CharaPtr& pChara ); - + void aggro( const CharaPtr& pChara ); void deaggro( const CharaPtr& pChara ); @@ -143,6 +146,7 @@ namespace Sapphire::Entity bool hasFlag( uint32_t flag ) const; void setFlag( uint32_t flags ); + void removeFlag( uint32_t flag ); void clearFlags(); void calculateStats() override; diff --git a/src/world/Encounter/EncounterTimeline.cpp b/src/world/Encounter/EncounterTimeline.cpp index c76f82ed..347c850f 100644 --- a/src/world/Encounter/EncounterTimeline.cpp +++ b/src/world/Encounter/EncounterTimeline.cpp @@ -69,6 +69,7 @@ namespace Sapphire { auto pBattleNpc = pTeri->getActiveBNpcByLayoutId( this->layoutId ); + // todo: these should really use callbacks when the state transitions or we could miss this tick switch( combatState ) { case CombatStateType::Idle: @@ -114,14 +115,7 @@ namespace Sapphire { case TimepointDataType::Idle: { - auto pIdleData = std::dynamic_pointer_cast< TimepointDataIdle, TimepointData >( getData() ); - auto pBNpc = pTeri->getActiveBNpcByLayoutId( pIdleData->m_layoutId ); - - if( pBNpc ) - { - // todo: idle - - } + // just wait up the duration of this timepoint } break; case TimepointDataType::CastAction: @@ -162,7 +156,7 @@ namespace Sapphire else { // if we are at the pos, stop waiting - state.m_finished = true; + //state.m_finished = true; } pBNpc->setRot( pMoveToData->m_rot ); } @@ -557,7 +551,8 @@ namespace Sapphire { "spawnBNpc", TimepointDataType::SpawnBNpc }, { "bNpcFlags", TimepointDataType::SetBNpcFlags }, { "setEObjState", TimepointDataType::SetEObjState }, - { "setCondition", TimepointDataType::SetCondition } + { "setCondition", TimepointDataType::SetCondition }, + { "snapshot", TimepointDataType::Snapshot } }; const static std::unordered_map< std::string, TimepointOverrideFlags > overrideFlagMap = @@ -565,19 +560,6 @@ namespace Sapphire {} }; - const static std::unordered_map< std::string, TargetSelectFilterFlags > targetFilterMap = - { - { "self", TargetSelectFilterFlags::Self }, - { "tank", TargetSelectFilterFlags::Tank }, - { "healer", TargetSelectFilterFlags::Healer }, - { "dps", TargetSelectFilterFlags::Dps }, - { "melee", TargetSelectFilterFlags::Melee }, - { "ranged", TargetSelectFilterFlags::Ranged }, - { "furthest", TargetSelectFilterFlags::Furthest }, - { "aggro1", TargetSelectFilterFlags::Aggro1 }, - { "aggro2", TargetSelectFilterFlags::Aggro2 } - }; - const static std::unordered_map< std::string, TimepointCallbackType > callbackTypeMap = { { "onActionInit", TimepointCallbackType::OnActionInit }, @@ -602,6 +584,12 @@ namespace Sapphire { "and", DirectorOpId::And } }; + const static std::unordered_map< std::string, Common::BNpcType > bnpcTypeMap = + { + { "bnpc", Common::BNpcType::Enemy }, + { "ally", Common::BNpcType::Friendly } // todo: rename this + }; + TimepointDataType tpType{ 0 }; auto typeStr = json.at( "type" ).get< std::string >(); @@ -727,7 +715,7 @@ namespace Sapphire auto& dataJ = json.at( "data" ); auto flags = dataJ.at( "val" ).get< uint32_t >(); auto opStr = dataJ.at( "opc" ).get< std::string >(); - DirectorOpId op = directorOpMap.find( opStr )->second; + DirectorOpId op = directorOpMap.at( opStr ); auto pDirectorData = std::make_shared< TimepointDataDirector >( tpType, op ); pDirectorData->m_data.flags = flags; @@ -753,6 +741,9 @@ namespace Sapphire auto hateSrcJ = dataJ.at( "hateSrc" ); auto actorRef = dataJ.at( "spawnActor" ).get< std::string >(); auto flags = dataJ.at( "flags" ).get< uint32_t >(); + // todo: batallion + // auto battalion = dataJ.at( "batallion" ).get< uint32_t >(); + auto bnpcType = bnpcTypeMap.at( dataJ.at( "type" ).get< std::string >() ); // todo: hateSrc @@ -762,7 +753,7 @@ namespace Sapphire else throw std::runtime_error( fmt::format( std::string( "EncounterTimeline::Timepoint::from_json: SpawnBNpc invalid actor ref: %s" ), actorRef ) ); - m_pData = std::make_shared< TimepointDataSpawnBNpc >( layoutId, flags ); + m_pData = std::make_shared< TimepointDataSpawnBNpc >( layoutId, flags, bnpcType ); } break; case TimepointDataType::SetBNpcFlags: diff --git a/src/world/Encounter/EncounterTimeline.h b/src/world/Encounter/EncounterTimeline.h index 3a67093e..e4a5d420 100644 --- a/src/world/Encounter/EncounterTimeline.h +++ b/src/world/Encounter/EncounterTimeline.h @@ -1,12 +1,12 @@ #include #include -#include + #include #include #include #include +#include #include -#include #include #include @@ -116,7 +116,9 @@ namespace Sapphire SetEObjState, SetBgm, - SetCondition + SetCondition, + + Snapshot }; enum class TimepointCallbackType : uint32_t @@ -193,7 +195,7 @@ namespace Sapphire std::vector < TimepointCallbackFunc > m_callbacks; }; using TimebackCallbackDataPtr = std::shared_ptr< TimepointCallbackData >; - using TimepointCallbacks = std::map< TimepointCallbackType, TimebackCallbackDataPtr >; + using TimepointCallbacks = std::unordered_map< TimepointCallbackType, TimebackCallbackDataPtr >; struct TimepointData : @@ -341,12 +343,15 @@ namespace Sapphire { uint32_t m_layoutId{ 0xE0000000 }; uint32_t m_flags{ 0 }; + uint32_t m_type{ 0 }; + // todo: hate type, source - TimepointDataSpawnBNpc( uint32_t layoutId, uint32_t flags ) : + TimepointDataSpawnBNpc( uint32_t layoutId, uint32_t flags, uint32_t type ) : TimepointData( TimepointDataType::SpawnBNpc ), m_layoutId( layoutId ), - m_flags( flags) + m_flags( flags ), + m_type( type ) { } }; @@ -615,13 +620,21 @@ namespace Sapphire }; using PhaseConditionPtr = std::shared_ptr< PhaseCondition >; + // todo: bnpc parts + class TimelineBNpcPart + { + uint32_t m_hp{ 0 }; + std::string m_name; + }; + class TimelineActor { protected: std::unordered_map< uint32_t, PhaseConditionPtr > m_phaseConditions; std::unordered_map< uint32_t, ConditionState > m_conditionStates; + // PARENTNAME_SUBACTOR_1, ..., PARENTNAME_SUBACTOR_69 - std::map< std::string, Entity::BNpcPtr > m_subActors; + std::unordered_map< std::string, Entity::BNpcPtr > m_subActors; public: uint32_t m_layoutId{ 0 }; uint32_t m_hp{ 0 };