1
Fork 0
mirror of https://github.com/SapphireServer/Sapphire.git synced 2025-05-02 00:47:45 +00:00

Merge pull request #915 from Skyliegirl33/actions

[3.x] Some initial action work (mostly statuses)
This commit is contained in:
Mordred 2023-03-08 09:21:39 +01:00 committed by GitHub
commit 801fc9da8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 314 additions and 48 deletions

View file

@ -398,7 +398,18 @@
"restorePercentage": 0,
"nextCombo": [],
"statuses": {
"caster": [],
"caster": [
{
"id": 83,
"duration": 20000,
"modifiers": [
{
"modifier": "DefensePercent",
"value": 20
}
]
}
],
"target": []
}
},
@ -414,7 +425,18 @@
"nextCombo": [],
"statuses": {
"caster": [],
"target": []
"target": [
{
"id": 244,
"duration": 30000,
"modifiers": [
{
"modifier": "TickDamage",
"value": 20
}
]
}
]
}
},
"34": {
@ -478,7 +500,18 @@
45
],
"statuses": {
"caster": [],
"caster": [
{
"id": 85,
"duration": 24000,
"modifiers": [
{
"modifier": "AttackPowerPercent",
"value": 20
}
]
}
],
"target": []
}
},
@ -493,7 +526,18 @@
"restorePercentage": 0,
"nextCombo": [],
"statuses": {
"caster": [],
"caster": [
{
"id": 86,
"duration": 20000,
"modifiers": [
{
"modifier": "AttackPowerPercent",
"value": 50
}
]
}
],
"target": []
}
},
@ -523,7 +567,18 @@
"restorePercentage": 0,
"nextCombo": [],
"statuses": {
"caster": [],
"caster": [
{
"id": 87,
"duration": 20000,
"modifiers": [
{
"modifier": "HPPercent",
"value": 20
}
]
}
],
"target": []
}
},
@ -2454,7 +2509,38 @@
"nextCombo": [],
"statuses": {
"caster": [],
"target": []
"target": [
{
"id": 180,
"duration": 24000,
"modifiers": [
{
"modifier": "TickDamage",
"value": 35
}
]
},
{
"id": 191,
"duration": 24000,
"modifiers": [
{
"modifier": "HealingRecoveryPercent",
"value": -20
}
]
},
{
"id": 240,
"duration": 24000,
"modifiers": [
{
"modifier": "HeavyPercent",
"value": 40
}
]
}
]
}
},
"169": {

View file

@ -887,22 +887,30 @@ namespace Sapphire::Common
Perception = 73,
// Unique modifiers
HPPercent = 1000,
MPPercent = 1001,
TPPercent = 1002,
GPPercent = 1003,
CPPercent = 1004,
PhysicalDamagePercent = 1005,
MagicDamagePercent = 1006,
AttackPowerPercent = 1007,
DefensePercent = 1008,
AccuracyPercent = 1009,
EvasionPercent = 1010,
MagicDefensePercent = 1011,
CriticalHitPowerPercent = 1012,
CriticalHitResiliencePercent = 1013,
CriticalHitPercent = 1014,
EnmityPercent = 1015
TickHeal = 1000,
TickDamage = 1001,
StrengthPercent = 1002,
DexterityPercent = 1003,
VitalityPercent = 1004,
IntelligencePercent = 1005,
MindPercent = 1006,
PietyPercent = 1007,
HPPercent = 1008,
MPPercent = 1009,
TPPercent = 1010,
GPPercent = 1011,
CPPercent = 1012,
PhysicalDamagePercent = 1013,
MagicDamagePercent = 1014,
AttackPowerPercent = 1015,
DefensePercent = 1016,
AccuracyPercent = 1017,
EvasionPercent = 1018,
MagicDefensePercent = 1019,
CriticalHitPowerPercent = 1020,
CriticalHitResiliencePercent = 1021,
CriticalHitPercent = 1022,
EnmityPercent = 1023
};
enum struct ActionAspect : uint8_t

View file

@ -40,6 +40,7 @@ struct StatusModifier
struct StatusEntry
{
uint16_t id;
int32_t duration;
std::vector< StatusModifier > modifiers;
};
@ -76,6 +77,7 @@ void to_json( nlohmann::ordered_json& j, const StatusEntry& statusEntry )
{
j = nlohmann::ordered_json{
{ "id", statusEntry.id },
{ "duration", statusEntry.duration },
{ "modifiers", statusEntry.modifiers }
};
}

View file

@ -431,7 +431,7 @@ void Action::Action::execute()
if( !hasClientsideTarget() )
{
buildEffects();
handleAction();
}
else if( auto player = m_pSource->getAsPlayer() )
{
@ -501,7 +501,15 @@ std::pair< uint32_t, Common::ActionHitSeverityType > Action::Action::calcHealing
return Math::CalcStats::calcActionHealing( *m_pSource, potency, wepDmg );
}
void Action::Action::buildEffects()
void Action::Action::applyStatusEffectSelf( uint16_t statusId, uint8_t param )
{
if( m_hitActors.size() > 0 )
getEffectbuilder()->applyStatusEffect( m_hitActors[ 0 ], statusId, param, true );
else
getEffectbuilder()->applyStatusEffect( m_pSource, statusId, param );
}
void Action::Action::handleAction()
{
snapshotAffectedActors( m_hitActors );
@ -593,6 +601,9 @@ void Action::Action::buildEffects()
}
}
if( m_lutEntry.statuses.caster.size() > 0 || m_lutEntry.statuses.target.size() > 0 )
handleStatusEffects();
m_effectBuilder->buildAndSendPackets( m_hitActors );
// TODO: disabled, reset kills our queued actions
@ -600,6 +611,38 @@ void Action::Action::buildEffects()
// m_effectBuilder.reset();
}
void Action::Action::handleStatusEffects()
{
if( isComboAction() && !isCorrectCombo() )
return;
// handle caster statuses
if( m_lutEntry.statuses.caster.size() > 0 )
{
for( auto& status : m_lutEntry.statuses.caster )
{
applyStatusEffectSelf( status.id );
m_pSource->addStatusEffectByIdIfNotExist( status.id, status.duration, *m_pSource, status.modifiers );
}
}
// handle hit actor statuses
if( m_lutEntry.statuses.target.size() > 0 && m_hitActors.size() > 0 )
{
for( auto& actor : m_hitActors )
{
for( auto& status : m_lutEntry.statuses.target )
{
getEffectbuilder()->applyStatusEffect( actor, status.id, 0 );
actor->addStatusEffectByIdIfNotExist( status.id, status.duration, *m_pSource, status.modifiers );
}
if( actor->getStatusEffectMap().size() > 0 )
actor->onActionHostile( m_pSource );
}
}
}
bool Action::Action::preCheck()
{
if( auto player = m_pSource->getAsPlayer() )
@ -864,7 +907,8 @@ Entity::CharaPtr Action::Action::getHitChara()
bool Action::Action::hasValidLutEntry() const
{
return m_lutEntry.potency != 0 || m_lutEntry.comboPotency != 0 || m_lutEntry.flankPotency != 0 || m_lutEntry.frontPotency != 0 ||
m_lutEntry.rearPotency != 0 || m_lutEntry.curePotency != 0 || m_lutEntry.restoreMPPercentage != 0;
m_lutEntry.rearPotency != 0 || m_lutEntry.curePotency != 0 || m_lutEntry.restoreMPPercentage != 0 ||
m_lutEntry.statuses.caster.size() > 0 || m_lutEntry.statuses.target.size() > 0;
}
Action::EffectBuilderPtr Action::Action::getEffectbuilder()

View file

@ -105,7 +105,11 @@ namespace Sapphire::World::Action
EffectBuilderPtr getEffectbuilder();
void buildEffects();
void applyStatusEffectSelf( uint16_t statusId, uint8_t param = 0 );
void handleAction();
void handleStatusEffects();
/*!
* @brief Adds an actor filter to this action.

View file

@ -15,7 +15,8 @@ bool ActionLut::validEntryExists( uint16_t actionId )
// if all of the fields are 0, it's not 'valid' due to parse error or no useful data in the tooltip
return entry.potency != 0 || entry.comboPotency != 0 || entry.flankPotency != 0 || entry.frontPotency != 0 ||
entry.rearPotency != 0 || entry.curePotency != 0;
entry.rearPotency != 0 || entry.curePotency != 0 ||
entry.statuses.caster.size() > 0 || entry.statuses.target.size() > 0;
}
const ActionEntry& ActionLut::getEntry( uint16_t actionId )

View file

@ -17,6 +17,7 @@ namespace Sapphire::World::Action
struct StatusEntry
{
uint16_t id;
int32_t duration;
std::vector< StatusModifier > modifiers;
};

View file

@ -85,6 +85,14 @@ std::unordered_map< std::string, Common::ParamModifier > ActionLutData::m_modifi
{ "Control", Common::ParamModifier::Control },
{ "Gathering", Common::ParamModifier::Gathering },
{ "Perception", Common::ParamModifier::Perception },
{ "TickHeal", Common::ParamModifier::TickHeal },
{ "TickDamage", Common::ParamModifier::TickDamage },
{ "StrengthPercent", Common::ParamModifier::StrengthPercent },
{ "DexterityPercent", Common::ParamModifier::DexterityPercent },
{ "VitalityPercent", Common::ParamModifier::VitalityPercent },
{ "IntelligencePercent", Common::ParamModifier::IntelligencePercent },
{ "MindPercent", Common::ParamModifier::MindPercent },
{ "PietyPercent", Common::ParamModifier::PietyPercent },
{ "HPPercent", Common::ParamModifier::HPPercent },
{ "MPPercent", Common::ParamModifier::MPPercent },
{ "TPPercent", Common::ParamModifier::TPPercent },

View file

@ -32,6 +32,7 @@ namespace Sapphire::World::Action
inline void from_json( const nlohmann::json& j, StatusEntry& statusEntry )
{
j.at( "id" ).get_to( statusEntry.id );
j.at( "duration" ).get_to( statusEntry.duration );
if( j.contains( "modifiers" ) )
j.at( "modifiers" ).get_to( statusEntry.modifiers );
}

View file

@ -93,10 +93,10 @@ void EffectBuilder::comboSucceed( Entity::CharaPtr& target )
addResultToActor( target, nextResult );
}
void EffectBuilder::applyStatusEffect( Entity::CharaPtr& target, uint16_t statusId, uint8_t param )
void EffectBuilder::applyStatusEffect( Entity::CharaPtr& target, uint16_t statusId, uint8_t param, bool forSelf )
{
EffectResultPtr nextResult = make_EffectResult( target, 0 );
nextResult->applyStatusEffect( statusId, param );
nextResult->applyStatusEffect( statusId, param, forSelf );
addResultToActor( target, nextResult );
}

View file

@ -25,7 +25,7 @@ namespace Sapphire::World::Action
void comboSucceed( Entity::CharaPtr& target );
void applyStatusEffect( Entity::CharaPtr& target, uint16_t statusId, uint8_t param );
void applyStatusEffect( Entity::CharaPtr& target, uint16_t statusId, uint8_t param, bool forSelf = false );
void mount( Entity::CharaPtr& target, uint16_t mountId );

View file

@ -70,11 +70,13 @@ void EffectResult::comboSucceed()
m_result.Type = Common::ActionEffectType::CALC_RESULT_TYPE_COMBO_HIT;
}
void EffectResult::applyStatusEffect( uint16_t statusId, uint8_t param )
void EffectResult::applyStatusEffect( uint16_t statusId, uint8_t param, bool forSelf )
{
m_result.Value = static_cast< int16_t >( statusId );
m_result.Arg2 = param;
m_result.Type = Common::ActionEffectType::CALC_RESULT_TYPE_SET_STATUS;
if( forSelf )
m_result.Flag = static_cast< uint8_t >( Common::ActionEffectResultFlag::EffectOnSource );
m_result.Type = forSelf ? Common::ActionEffectType::CALC_RESULT_TYPE_SET_STATUS_ME : Common::ActionEffectType::CALC_RESULT_TYPE_SET_STATUS;
}
void EffectResult::mount( uint16_t mountId )

View file

@ -19,7 +19,7 @@ namespace Sapphire::World::Action
void restoreMP( uint32_t amount, Common::ActionEffectResultFlag flag = Common::ActionEffectResultFlag::None );
void startCombo( uint16_t actionId );
void comboSucceed();
void applyStatusEffect( uint16_t statusId, uint8_t param );
void applyStatusEffect( uint16_t statusId, uint8_t param, bool forSelf );
void mount( uint16_t mountId );
Entity::CharaPtr getTarget() const;

View file

@ -5,7 +5,6 @@
#include <Network/CommonActorControl.h>
#include <Service.h>
#include "Forwards.h"
#include "Territory/Territory.h"
@ -26,6 +25,7 @@
#include "Player.h"
#include "Manager/TerritoryMgr.h"
#include "Manager/MgrUtil.h"
#include "Manager/PlayerMgr.h"
#include "Common.h"
using namespace Sapphire::Common;
@ -559,7 +559,17 @@ void Sapphire::Entity::Chara::addStatusEffectByIdIfNotExist( uint32_t id, int32_
auto effect = StatusEffect::make_StatusEffect( id, source.getAsChara(), getAsChara(), duration, 3000 );
effect->setParam( param );
addStatusEffect( effect );
}
void Sapphire::Entity::Chara::addStatusEffectByIdIfNotExist( uint32_t id, int32_t duration, Entity::Chara& source,
std::vector< World::Action::StatusModifier >& modifiers, uint16_t param )
{
if( hasStatusEffect( id ) )
return;
auto effect = StatusEffect::make_StatusEffect( id, source.getAsChara(), getAsChara(), duration, modifiers, 3000 );
effect->setParam( param );
addStatusEffect( effect );
}
int8_t Sapphire::Entity::Chara::getStatusEffectFreeSlot()
@ -644,7 +654,6 @@ void Sapphire::Entity::Chara::sendStatusEffectUpdate()
{
uint64_t currentTimeMs = Util::getTimeMs();
auto statusEffectList = makeZonePacket< FFXIVIpcStatus >( getId() );
uint8_t slot = 0;
for( const auto& effectIt : m_statusEffectMap )
@ -689,7 +698,13 @@ void Sapphire::Entity::Chara::updateStatusEffects()
bool Sapphire::Entity::Chara::hasStatusEffect( uint32_t id )
{
return m_statusEffectMap.find( id ) != m_statusEffectMap.end();
for( const auto& [ key, val ] : m_statusEffectMap )
{
if( val->getId() == id )
return true;
}
return false;
}
int64_t Sapphire::Entity::Chara::getLastUpdateTime() const
@ -785,6 +800,47 @@ void Sapphire::Entity::Chara::setStatValue( Common::BaseParam baseParam, uint32_
m_baseStats[ index ] = value;
}
float Sapphire::Entity::Chara::getModifier( Common::ParamModifier paramModifier ) const
{
if( m_modifiers.find( paramModifier ) == m_modifiers.end() )
return paramModifier >= Common::ParamModifier::StrengthPercent ? 1.0f : 0;
auto& mod = m_modifiers.at( paramModifier );
if( paramModifier >= Common::ParamModifier::StrengthPercent )
{
auto valPercent = 1.0f;
for( const auto val : mod )
valPercent *= 1.0f + ( val / 100.0f );
return valPercent;
}
else
{
return std::accumulate( mod.begin(), mod.end(), 0 );
}
}
void Sapphire::Entity::Chara::addModifier( Common::ParamModifier paramModifier, int32_t value )
{
m_modifiers[ paramModifier ].push_back( value );
if( auto pPlayer = this->getAsPlayer(); pPlayer )
Common::Service< World::Manager::PlayerMgr >::ref().sendDebug( *pPlayer, "Modifier: {}, value: {}", static_cast< int32_t >( paramModifier ), getModifier( paramModifier ) );
}
void Sapphire::Entity::Chara::delModifier( Common::ParamModifier paramModifier, int32_t value )
{
assert( m_modifiers.count( paramModifier ) != 0 );
auto& mod = m_modifiers.at( paramModifier );
mod.erase( std::remove( mod.begin(), mod.end(), value ), mod.end() );
if( mod.size() == 0 )
m_modifiers.erase( paramModifier );
if( auto pPlayer = this->getAsPlayer(); pPlayer )
Common::Service< World::Manager::PlayerMgr >::ref().sendDebug( *pPlayer, "Modifier: {}, value: {}", static_cast< int32_t >( paramModifier ), getModifier( paramModifier ) );
}
void Sapphire::Entity::Chara::onTick()
{
uint32_t thisTickDmg = 0;
@ -795,13 +851,13 @@ void Sapphire::Entity::Chara::onTick()
auto thisEffect = effectIt.second->getTickEffect();
switch( thisEffect.first )
{
case 1:
case Common::ParamModifier::TickDamage:
{
thisTickDmg += thisEffect.second;
break;
}
case 2:
case Common::ParamModifier::TickHeal:
{
thisTickHeal += thisEffect.second;
break;
@ -809,12 +865,18 @@ void Sapphire::Entity::Chara::onTick()
}
}
// TODO: don't really like how this is handled
// TODO: calculate actual damage from potency
if( thisTickDmg != 0 )
{
takeDamage( thisTickDmg );
server().queueForPlayers( getInRangePlayerIds( isPlayer() ), makeActorControl( getId(), HPFloatingText, 0,
static_cast< uint8_t >( ActionEffectType::CALC_RESULT_TYPE_DAMAGE_HP ),
thisTickDmg ) );
if( isPlayer() )
server().queueForPlayers( getInRangePlayerIds( isPlayer() ), makeHudParam( *getAsPlayer() ) );
else if( isBattleNpc() )
server().queueForPlayers( getInRangePlayerIds( isPlayer() ), makeHudParam( *getAsBNpc() ) );
}
if( thisTickHeal != 0 )
@ -823,5 +885,9 @@ void Sapphire::Entity::Chara::onTick()
server().queueForPlayers( getInRangePlayerIds( isPlayer() ), makeActorControl( getId(), HPFloatingText, 0,
static_cast< uint8_t >( ActionEffectType::CALC_RESULT_TYPE_RECOVER_HP ),
thisTickHeal ) );
if( isPlayer() )
server().queueForPlayers( getInRangePlayerIds( isPlayer() ), makeHudParam( *getAsPlayer() ) );
else if( isBattleNpc() )
server().queueForPlayers( getInRangePlayerIds( isPlayer() ), makeHudParam( *getAsBNpc() ) );
}
}

View file

@ -1,6 +1,7 @@
#pragma once
#include <Common.h>
#include "Action/ActionLut.h"
#include "Forwards.h"
#include "GameObject.h"
@ -8,6 +9,7 @@
#include <map>
#include <queue>
#include <array>
#include <numeric>
namespace Sapphire::Entity
{
@ -26,10 +28,13 @@ namespace Sapphire::Entity
public:
using ActorStatsArray = std::array< uint32_t, STAT_ARRAY_SIZE >;
using ActorModifiersMap = std::unordered_map< Common::ParamModifier, std::vector< int32_t > >;
ActorStatsArray m_baseStats{ 0 };
ActorStatsArray m_bonusStats{ 0 };
ActorModifiersMap m_modifiers{ 0 };
protected:
char m_name[34];
/*! Last tick time for the actor ( in ms ) */
@ -133,9 +138,9 @@ namespace Sapphire::Entity
// add a status effect by id
void addStatusEffectById( uint32_t id, int32_t duration, Entity::Chara& source, uint16_t param = 0 );
// add a status effect by id if it doesn't exist
void addStatusEffectByIdIfNotExist( uint32_t id, int32_t duration, Entity::Chara& source, uint16_t param = 0 );
void addStatusEffectByIdIfNotExist( uint32_t id, int32_t duration, Entity::Chara& source, std::vector< World::Action::StatusModifier >& modifiers, uint16_t param = 0 );
// remove a status effect by id
void removeSingleStatusEffectFromId( uint32_t id );
@ -159,6 +164,12 @@ namespace Sapphire::Entity
void setStatValue( Common::BaseParam baseParam, uint32_t value );
float getModifier( Common::ParamModifier paramModifier ) const;
void addModifier( Common::ParamModifier paramModifier, int32_t value );
void delModifier( Common::ParamModifier paramModifier, int32_t value );
uint32_t getHp() const;
uint32_t getHpPercent() const;

View file

@ -17,12 +17,20 @@ using namespace Sapphire::Common;
using namespace Sapphire::Network::Packets;
//using namespace Sapphire::Network::Packets::WorldPackets::Server;
Sapphire::StatusEffect::StatusEffect::StatusEffect( uint32_t id, Entity::CharaPtr sourceActor, Entity::CharaPtr targetActor,
uint32_t duration, std::vector< World::Action::StatusModifier >& modifiers, uint32_t tickRate ) :
StatusEffect( id, sourceActor, targetActor, duration, tickRate )
{
m_modifiers = std::move( modifiers );
}
Sapphire::StatusEffect::StatusEffect::StatusEffect( uint32_t id, Entity::CharaPtr sourceActor, Entity::CharaPtr targetActor,
uint32_t duration, uint32_t tickRate ) :
m_id( id ),
m_sourceActor( sourceActor ),
m_targetActor( targetActor ),
m_duration( duration ),
m_modifiers( 0 ),
m_startTime( 0 ),
m_tickRate( tickRate ),
m_lastTick( 0 )
@ -47,16 +55,14 @@ Sapphire::StatusEffect::StatusEffect::~StatusEffect()
{
}
void Sapphire::StatusEffect::StatusEffect::registerTickEffect( uint8_t type, uint32_t param )
void Sapphire::StatusEffect::StatusEffect::registerTickEffect( ParamModifier type, uint32_t param )
{
m_currTickEffect = std::make_pair( type, param );
}
std::pair< uint8_t, uint32_t > Sapphire::StatusEffect::StatusEffect::getTickEffect()
std::pair< ParamModifier, uint32_t > Sapphire::StatusEffect::StatusEffect::getTickEffect()
{
auto thisTick = m_currTickEffect;
m_currTickEffect = std::make_pair( 0, 0 );
return thisTick;
return m_currTickEffect;
}
void Sapphire::StatusEffect::StatusEffect::onTick()
@ -87,6 +93,19 @@ void Sapphire::StatusEffect::StatusEffect::applyStatus()
m_startTime = Util::getTimeMs();
auto& scriptMgr = Common::Service< Scripting::ScriptMgr >::ref();
for( const auto& mod : m_modifiers )
{
// TODO: ticks
if( mod.modifier != Common::ParamModifier::TickDamage && mod.modifier != Common::ParamModifier::TickHeal )
m_targetActor->addModifier( mod.modifier, mod.value );
else if( mod.modifier == Common::ParamModifier::TickDamage )
registerTickEffect( mod.modifier, mod.value );
else if( mod.modifier == Common::ParamModifier::TickHeal )
registerTickEffect( mod.modifier, mod.value );
}
m_targetActor->calculateStats();
// this is only right when an action is being used by the player
// else you probably need to use an actorcontrol
@ -111,6 +130,15 @@ void Sapphire::StatusEffect::StatusEffect::applyStatus()
void Sapphire::StatusEffect::StatusEffect::removeStatus()
{
auto& scriptMgr = Common::Service< Scripting::ScriptMgr >::ref();
for( const auto& mod : m_modifiers )
{
if( mod.modifier != Common::ParamModifier::TickDamage && mod.modifier != Common::ParamModifier::TickHeal )
m_targetActor->delModifier( mod.modifier, mod.value );
}
m_targetActor->calculateStats();
scriptMgr.onStatusTimeOut( m_targetActor, m_id );
}

View file

@ -2,6 +2,7 @@
#define _STATUSEFFECT_H_
#include "Forwards.h"
#include "Action/ActionLut.h"
namespace Sapphire {
namespace StatusEffect {
@ -10,6 +11,9 @@ namespace StatusEffect {
class StatusEffect
{
public:
StatusEffect( uint32_t id, Entity::CharaPtr sourceActor, Entity::CharaPtr targetActor,
uint32_t duration, std::vector< World::Action::StatusModifier >& modifiers, uint32_t tickRate );
StatusEffect( uint32_t id, Entity::CharaPtr sourceActor, Entity::CharaPtr targetActor,
uint32_t duration, uint32_t tickRate );
@ -41,9 +45,9 @@ public:
void setParam( uint16_t param );
void registerTickEffect( uint8_t type, uint32_t param );
void registerTickEffect( Common::ParamModifier type, uint32_t param );
std::pair< uint8_t, uint32_t > getTickEffect();
std::pair< Common::ParamModifier, uint32_t > getTickEffect();
const std::string& getName() const;
@ -57,8 +61,8 @@ private:
uint64_t m_lastTick;
uint16_t m_param;
std::string m_name;
std::pair< uint8_t, uint32_t > m_currTickEffect;
std::pair< Common::ParamModifier, uint32_t > m_currTickEffect;
std::vector< World::Action::StatusModifier > m_modifiers;
};
}