1232 lines
33 KiB
C++
1232 lines
33 KiB
C++
// Project Lab - NHTV Igad
|
|
#include "UnrealProject.h"
|
|
#if PLATFORM_SPECIFIC_WIN == 0
|
|
#include "InputManager.hpp"
|
|
using namespace Input;
|
|
#endif
|
|
|
|
#include "NetworkCharacter.h"
|
|
|
|
#include "AbilityInfo.h"
|
|
#include "ItemBase.h"
|
|
#include "BaseSkillObject.h"
|
|
#include "IngameSkillTree.h"
|
|
#include "DefaultPlayerController.h"
|
|
#include "DefaultGameMode.h"
|
|
#include "NetworkPlayer.h"
|
|
#include "Modifier.h"
|
|
#include "AbilityEventGroup.h"
|
|
#include "PreCastAbilityEventGroup.h"
|
|
#include "NativeModifiers.h"
|
|
#include "DefaultGameState.h"
|
|
#include "DefaultPlayerState.h"
|
|
#include "Effect.h"
|
|
|
|
TSubclassOf<ATargetedEffect> expGainEffect;
|
|
|
|
ANetworkCharacter::ANetworkCharacter()
|
|
{
|
|
expGainEffect = ConstructorHelpers::FClassFinder<ATargetedEffect>(L"/Game/Assets/Art/Effects/FX_ExpParticle").Class;
|
|
|
|
// Set size for collision capsule
|
|
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
|
|
|
|
// Network init
|
|
bReplicates = true;
|
|
bAlwaysRelevant = true;
|
|
bReplicateMovement = true;
|
|
|
|
// Configure character movement
|
|
GetCharacterMovement()->bOrientRotationToMovement = true; // Rotate character to moving direction
|
|
GetCharacterMovement()->RotationRate = FRotator(0.f, 640.f, 0.f);
|
|
GetCharacterMovement()->bConstrainToPlane = true;
|
|
GetCharacterMovement()->bSnapToPlaneAtStart = true;
|
|
baseMovementSpeed = GetCharacterMovement()->MaxWalkSpeed;
|
|
|
|
experienceGainOnKill = 50;
|
|
m_maxHealth = 1000;
|
|
m_maxMana = 100;
|
|
canBeStunned = true;
|
|
m_visible = true;
|
|
m_usesMana = true;
|
|
|
|
// Default to neutral
|
|
GetMesh()->bRenderCustomDepth = true;
|
|
GetMesh()->CustomDepthStencilValue = 0;
|
|
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
m_lastPlayerDamage = nullptr;
|
|
|
|
|
|
healthAccumTimer = 1.0f;
|
|
damageAccumTimer = 0.1f;
|
|
|
|
teamDamageTimer = 5.0f;
|
|
|
|
m_healAccumValue = 0;
|
|
m_healAccumTimer = 0;
|
|
}
|
|
|
|
void ANetworkCharacter::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
m_cooldownReduction = 0.0f;
|
|
m_channelStun = false;
|
|
m_channelGroup = nullptr;
|
|
m_castingAbility = nullptr;
|
|
m_initialised = true;
|
|
m_hitable = true;
|
|
|
|
m_silenceCount = 0;
|
|
m_silenced = false;
|
|
swingAnimationSequence = 0;
|
|
|
|
// Filter out NULL abilities
|
|
TArray<class UAbilityInfo*> actualAbilities;
|
|
for (int32 i = 0; i < abilities.Num(); i++)
|
|
{
|
|
if (!IsValid(abilities[i]))
|
|
continue;
|
|
actualAbilities.Add(abilities[i]);
|
|
}
|
|
abilities = actualAbilities;
|
|
|
|
|
|
for (int32 i = 0; i < abilities.Num(); i++)
|
|
{
|
|
GetAbilityState(abilities[i]);
|
|
}
|
|
|
|
RegisterHealthBar();
|
|
|
|
// Initialize the character modifiers on the server
|
|
if (Role == ROLE_Authority)
|
|
{
|
|
m_modifierManager = new ModifierManager(this);
|
|
m_SpawnModifiers();
|
|
for(UAbilityInfo* info : passives)
|
|
{
|
|
CastAbility(info);
|
|
}
|
|
m_health = m_maxHealth;
|
|
m_mana = m_maxMana;
|
|
}
|
|
}
|
|
|
|
void ANetworkCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
{
|
|
Super::EndPlay(EndPlayReason);
|
|
|
|
if (m_modifierManager)
|
|
{
|
|
delete m_modifierManager;
|
|
m_modifierManager = nullptr;
|
|
}
|
|
|
|
#if UE_INCLUDE_METRICS
|
|
// Record trivial player death
|
|
if(metricsHandle && EndPlayReason == EEndPlayReason::Type::Destroyed)
|
|
metricsHandle->OnPlayerDie(0);
|
|
#endif
|
|
}
|
|
|
|
void ANetworkCharacter::Destroyed()
|
|
{
|
|
UnregisterHealthBar();
|
|
Super::Destroyed();
|
|
}
|
|
|
|
void ANetworkCharacter::RegisterHealthBar()
|
|
{
|
|
// Notify UI of character spawn
|
|
ULocalPlayer* localPlayer = GetGameInstance()->GetFirstGamePlayer();
|
|
if(localPlayer)
|
|
{
|
|
ADefaultPlayerController* localPlayerController = Cast<ADefaultPlayerController>(localPlayer->PlayerController);
|
|
if(localPlayerController)
|
|
{
|
|
localPlayerController->OnCharacterCreated(this);
|
|
|
|
}
|
|
else
|
|
{
|
|
GWWARNING(L"No local player found to send OnCharacterCreated");
|
|
}
|
|
}
|
|
}
|
|
void ANetworkCharacter::UnregisterHealthBar()
|
|
{
|
|
// Notify UI of character destroy
|
|
UGameInstance* inst = GetGameInstance();
|
|
if(!inst)
|
|
return;
|
|
ULocalPlayer* player = inst->GetFirstGamePlayer();
|
|
if(player)
|
|
{
|
|
APlayerController* controller = player->PlayerController;
|
|
|
|
ADefaultPlayerController* localPlayer = Cast<ADefaultPlayerController>(controller);
|
|
if(localPlayer)
|
|
{
|
|
localPlayer->OnCharacterDestroyed(this);
|
|
}
|
|
else
|
|
{
|
|
GWWARNING(L"No local player found to send OnCharacterDestroyed");
|
|
}
|
|
}
|
|
}
|
|
void ANetworkCharacter::Tick(float DeltaSeconds)
|
|
{
|
|
Super::Tick(DeltaSeconds);
|
|
|
|
// Update the damage accumolation table
|
|
if (m_damageAccum.size() > 0)
|
|
{
|
|
for (auto iter = m_damageAccum.begin(); iter != m_damageAccum.end();)
|
|
{
|
|
// Controller no longer exists
|
|
if (!IsValid(iter->first))
|
|
{
|
|
iter = m_damageAccum.erase(iter);
|
|
continue;
|
|
}
|
|
|
|
DamagePeriod& value = iter->second;
|
|
value.timer -= DeltaSeconds;
|
|
|
|
// Time has expired
|
|
if (value.timer <= 0)
|
|
{
|
|
if (value.damage > 0)
|
|
iter->first->SpawnArcingCombatText(GetActorLocation(), FString::FromInt(value.damage), FLinearColor::White);
|
|
m_totalDamage = value.damage;
|
|
iter = m_damageAccum.erase(iter);
|
|
continue;
|
|
}
|
|
|
|
iter++;
|
|
}
|
|
}
|
|
|
|
// Update the team damage dealt table
|
|
if (m_teamDamageDealt.size() > 0)
|
|
{
|
|
for (auto iter = m_teamDamageDealt.begin(); iter != m_teamDamageDealt.end();)
|
|
{
|
|
DamagePeriod& value = iter->second;
|
|
value.timer -= DeltaSeconds;
|
|
|
|
// Time has expired, this team is no longer entitled for the experience
|
|
if (value.timer <= 0)
|
|
{
|
|
iter = m_teamDamageDealt.erase(iter);
|
|
continue;
|
|
}
|
|
|
|
iter++;
|
|
}
|
|
}
|
|
|
|
ADefaultPlayerController* const controller = Cast<ADefaultPlayerController>(GetController());
|
|
|
|
// Update the heal accumolation
|
|
if (IsValid(controller) && m_healAccumValue > 0)
|
|
{
|
|
m_healAccumTimer -= DeltaSeconds;
|
|
if (m_healAccumTimer <= 0)
|
|
{
|
|
m_healAccumTimer = 0;
|
|
controller->SpawnCombatText(GetActorLocation(), FString::FromInt(m_healAccumValue), FLinearColor::Green);
|
|
m_healAccumValue = 0;
|
|
}
|
|
}
|
|
|
|
if(GetMesh())
|
|
{
|
|
// Check if ally or player for silhouette material
|
|
// (0 = neutral, 1 = player, 2 = ally, 3 = enemy)
|
|
UWorld* const world = GetWorld();
|
|
const int32 current = GetMesh()->CustomDepthStencilValue;
|
|
int32 target = 0; // Default to neutral
|
|
|
|
if(controller == world->GetFirstPlayerController())
|
|
target = 1; // Self
|
|
else
|
|
{
|
|
// Get the local team
|
|
int32 localTeam = -1;
|
|
ADefaultPlayerController* controller = Cast<ADefaultPlayerController>(world->GetFirstPlayerController());
|
|
if(IsValid(controller))
|
|
{
|
|
APlayerStateBase* playerState = Cast<ADefaultPlayerState>(controller->PlayerState);
|
|
if(IsValid(playerState))
|
|
localTeam = playerState->GetTeam();
|
|
}
|
|
if(localTeam != -1)
|
|
{
|
|
if(GetTeam() == localTeam)
|
|
target = 2; // Ally
|
|
else if(GetTeam() != 0)
|
|
target = 3; // Enemy
|
|
}
|
|
}
|
|
if(target != current)
|
|
{
|
|
GetMesh()->CustomDepthStencilValue = target;
|
|
GetMesh()->MarkRenderStateDirty();
|
|
}
|
|
}
|
|
|
|
m_TickAbilities(DeltaSeconds);
|
|
|
|
#if UE_INCLUDE_METRICS
|
|
if (metricsHandle)
|
|
{
|
|
// Record player stats
|
|
metricsHandle->OnPlayerUpdate(GetActorLocation().X, GetActorLocation().Y);
|
|
ADefaultPlayerState* const state = Cast<ADefaultPlayerState>(PlayerState);
|
|
if(state)
|
|
metricsHandle->OnPlayerLevel(uint8(state->GetLevel()));
|
|
metricsHandle->OnPlayerMaxHealth(m_maxHealth);
|
|
metricsHandle->OnPlayerMaxMana(m_maxMana);
|
|
metricsHandle->OnPlayerHealth(m_health);
|
|
metricsHandle->OnPlayerMana(m_mana);
|
|
}
|
|
#endif
|
|
if (m_shouldBeDestroyed)
|
|
Destroy();
|
|
}
|
|
void ANetworkCharacter::OnDeath(UAbilityInfo* ability)
|
|
{
|
|
m_shouldBeDestroyed = true;
|
|
}
|
|
void ANetworkCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
|
|
DOREPLIFETIME(ANetworkCharacter, basicAttack);
|
|
DOREPLIFETIME(ANetworkCharacter, m_health);
|
|
DOREPLIFETIME(ANetworkCharacter, m_maxHealth);
|
|
DOREPLIFETIME(ANetworkCharacter, m_mana);
|
|
DOREPLIFETIME(ANetworkCharacter, m_maxMana);
|
|
DOREPLIFETIME(ANetworkCharacter, m_stunned);
|
|
DOREPLIFETIME(ANetworkCharacter, m_silenced);
|
|
DOREPLIFETIME(ANetworkCharacter, m_channelStun);
|
|
DOREPLIFETIME(ANetworkCharacter, m_allowChannelRotation);
|
|
DOREPLIFETIME(ANetworkCharacter, m_castingAbility);
|
|
DOREPLIFETIME(ANetworkCharacter, m_cooldownReduction);
|
|
DOREPLIFETIME(ANetworkCharacter, m_attackSpeed);
|
|
DOREPLIFETIME(ANetworkCharacter, m_attackDamageMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, m_magicDamageMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, m_blockedMana);
|
|
DOREPLIFETIME(ANetworkCharacter, m_damageMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, m_ignoreArmor);
|
|
DOREPLIFETIME(ANetworkCharacter, m_armor);
|
|
DOREPLIFETIME(ANetworkCharacter, m_manaRegenMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, m_visible);
|
|
DOREPLIFETIME(ANetworkCharacter, m_hitable);
|
|
DOREPLIFETIME(ANetworkCharacter, m_manaUsageMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, m_positiveEffectMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, m_negativeEffectMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, m_castingMovementspeedMultiplier);
|
|
DOREPLIFETIME(ANetworkCharacter, swingAnimationSequence);
|
|
}
|
|
|
|
void ANetworkCharacter::TriggerSwingAnimation_Server()
|
|
{
|
|
check(Role == ROLE_Authority);
|
|
swingAnimationSequence++;
|
|
}
|
|
|
|
void ANetworkCharacter::LearnSkills_Implementation(const TArray<FIngameSkillTreeSkill>& skills)
|
|
{
|
|
ADefaultPlayerController* pc = Cast<ADefaultPlayerController>(GetController());
|
|
|
|
for(size_t i = 0; i < skills.Num(); i++)
|
|
{
|
|
const FIngameSkillTreeSkill& obj = skills[i];
|
|
UAbilityInfo* ability = obj.selectedEffect;
|
|
|
|
if(ability)
|
|
{
|
|
// Initialize ability power in the ability state
|
|
AAbilityState* aState = GetAbilityState(ability);
|
|
if(aState)
|
|
aState->power = obj.power;
|
|
|
|
if(ability->passive)
|
|
{
|
|
if(!passives.Contains(ability))
|
|
{
|
|
// Learn passive
|
|
passives.Add(obj.selectedEffect);
|
|
|
|
// Only Cast the actual passive ability on the server
|
|
if(Role == ROLE_Authority)
|
|
{
|
|
CastAbility(ability);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Learn ability
|
|
if(!abilities.Contains(ability))
|
|
{
|
|
abilities.Add(ability);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
void ANetworkCharacter::LearnSkills_Server(const TArray<FIngameSkillTreeSkill>& skills)
|
|
{
|
|
// Server side call
|
|
LearnSkills_Implementation(skills);
|
|
// Client side call
|
|
LearnSkills(skills);
|
|
}
|
|
|
|
void ANetworkCharacter::m_SpawnModifiers()
|
|
{
|
|
ARegenModifier* regenMod = Cast<ARegenModifier>(m_modifierManager->AddModifier(ARegenModifier::StaticClass(), 0));
|
|
regenMod->regenPerTick = passiveHealthRegen;
|
|
AManaRegenModifier* manaMod = Cast<AManaRegenModifier>(m_modifierManager->AddModifier(AManaRegenModifier::StaticClass(), 0));
|
|
manaMod->regenPerTick = passiveManaRegen;
|
|
}
|
|
|
|
bool ANetworkCharacter::m_AllowedToMove()
|
|
{
|
|
return !IsStunned();
|
|
}
|
|
bool ANetworkCharacter::m_AllowedToRotate()
|
|
{
|
|
return CanRotate();
|
|
}
|
|
|
|
void ANetworkCharacter::m_OnAbilityCastConfirm_Implementation(UAbilityInfo* ability, bool success, bool toggleState, float cooldown, float cooldownTime)
|
|
{
|
|
if (!ability)
|
|
{
|
|
GWWARNING(L"Out of range value received for OnAbilityCastConfirm");
|
|
return;
|
|
}
|
|
|
|
AAbilityState* state = GetAbilityState(ability);
|
|
|
|
//GWPRINT(L"Ability toggle " + ability->GetName() + L" toggles from " + state->toggleState + L" to " + toggleState);
|
|
|
|
state->toggleState = toggleState;
|
|
|
|
state->onCooldownTimer = cooldown;
|
|
state->currentCooldownDuration = cooldownTime;
|
|
state->cooldownRate = cooldown/cooldownTime;
|
|
}
|
|
bool ANetworkCharacter::m_CastAbility_Server_Validate(UAbilityInfo* ability)
|
|
{
|
|
if(ability == nullptr)
|
|
return false;
|
|
if(ability->events.Num() == 0)
|
|
{
|
|
GWERROR(L"No events in ability->events for ability " + ability->GetName());
|
|
return false;
|
|
}
|
|
// Validate AbilityInfo Array
|
|
for(int32 i = 0; i < ability->events.Num(); i++)
|
|
{
|
|
if(ability->events[i] == nullptr)
|
|
{
|
|
GWERROR(L"Null entry in ability->events[" + i + L"] for ability " + ability->GetName());
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
void ANetworkCharacter::m_CastAbility_Server_Implementation(UAbilityInfo* ability)
|
|
{
|
|
AAbilityState* state = GetAbilityState(ability);
|
|
|
|
// Check if this is a toggle ability, and is toggled off
|
|
bool toggleOffAbility = ability->IsHoldOrToggle() && state->toggleState == true;
|
|
|
|
// Check if we're not channeling (if this is not a toggle off ability)
|
|
if(IsPendingKill() || IsPendingKillPending())
|
|
return;
|
|
|
|
// Check if stunned or already casting an ability
|
|
// allow if this is a toggle ability and currently on
|
|
if(!toggleOffAbility && (IsStunned() || IsChanneling() ))
|
|
{
|
|
m_OnAbilityCastConfirm_Server(ability, false);
|
|
return;
|
|
}
|
|
|
|
// Check if silenced, block all non-basic attacks in this case
|
|
// also, don't block passives
|
|
if (ability->abilityType != EAbilityType::Basic && IsSilenced() && !ability->passive)
|
|
{
|
|
m_OnAbilityCastConfirm_Server(ability, false);
|
|
return;
|
|
}
|
|
|
|
// Check if ability is not on cooldown (if not toggling off)
|
|
if(state->onCooldownTimer > 0.1f && !toggleOffAbility)
|
|
{
|
|
m_OnAbilityCastConfirm_Server(ability, false);
|
|
return;
|
|
}
|
|
|
|
// Use mana state for current ability / character mana usage chained together with ability state (toggle off ability?)
|
|
bool currentUsesMana = m_usesMana && !toggleOffAbility;
|
|
|
|
UWorld* const world = GetWorld();
|
|
if (ability && world && (ability->mana * m_manaUsageMultiplier <= m_mana || !currentUsesMana))
|
|
{
|
|
if (currentUsesMana)
|
|
RemoveMana(ability->mana * m_manaUsageMultiplier);
|
|
|
|
// Handle cooldown setting
|
|
if(ability->IsHoldOrToggle() && state->toggleState == false)
|
|
{
|
|
// No cooldown when casting toggle abilities
|
|
state->onCooldownTimer = 0.0f;
|
|
state->cooldownRate = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
// Go on cooldown if it is a toggle ability, or a regular ability
|
|
m_SetAbilityCooldown(ability, state);
|
|
}
|
|
|
|
if(ability->IsHoldOrToggle())
|
|
{
|
|
AAbilityEventGroup** activeAbility = m_activeAbilities.Find(ability);
|
|
bool isAbilityActive = activeAbility && *activeAbility;
|
|
|
|
// Check if ability is enabled
|
|
if(state->toggleState == false)
|
|
{
|
|
// Check if ability does not yet exist
|
|
if(!isAbilityActive)
|
|
{
|
|
state->toggleState = true;
|
|
|
|
// Activate toggle ability
|
|
m_InitAbilitySequence(ability, state);
|
|
}
|
|
else
|
|
{
|
|
m_OnAbilityCastConfirm_Server(ability, false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Deactivate ability
|
|
if(isAbilityActive)
|
|
{
|
|
state->toggleState = false;
|
|
(*activeAbility)->NextGroup();
|
|
*activeAbility = nullptr;
|
|
m_OnAbilityCastConfirm_Server(ability, true);
|
|
}
|
|
else
|
|
{
|
|
m_OnAbilityCastConfirm_Server(ability, false);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_InitAbilitySequence(ability, state);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_OnAbilityCastConfirm_Server(ability, false);
|
|
}
|
|
}
|
|
void ANetworkCharacter::m_InitAbilitySequence(class UAbilityInfo* ability, class AAbilityState* state)
|
|
{
|
|
AAbilityEventGroup* group = AAbilityEventGroup::SpawnSequence(this, ability, state, ability->events);
|
|
check(group);
|
|
if(group->channelEvent)
|
|
{
|
|
//GWPRINT(L"Begin channel ability " + group->GetName());
|
|
m_channelGroup = group;
|
|
m_castingAbility = ability;
|
|
m_allowChannelRotation = group->allowRotateWhileChannel;
|
|
if(m_castingMovementspeedMultiplier == 0.0f || ability->abilityType == EAbilityType::Basic)
|
|
m_channelStun = group->stunWhileChannel;
|
|
else
|
|
{
|
|
if (group->stunWhileChannel)
|
|
{
|
|
ASpeedModifier* sModifier = GetWorld()->SpawnActor<ASpeedModifier>();
|
|
sModifier->lifeTime = group->duration;
|
|
sModifier->speedMultiplier = m_castingMovementspeedMultiplier;
|
|
sModifier->target = this;
|
|
GetModifierManager()->AddModifier(sModifier);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_allowChannelRotation = false;
|
|
m_channelGroup = nullptr;
|
|
}
|
|
|
|
group->onAbilityEventGroupEnded.AddDynamic(this, &ANetworkCharacter::m_OnAbilityEventGroupEnded);
|
|
m_activeAbilities.Add(ability, group);
|
|
|
|
m_OnAbilityCastConfirm_Server(ability, true);
|
|
}
|
|
|
|
void ANetworkCharacter::m_PreCast(UAbilityInfo* ability)
|
|
{
|
|
if(ability->mana * m_manaUsageMultiplier <= m_mana)
|
|
m_currentPreCast = APreCastAbilityEventGroup::InitPreCast(ability, this);
|
|
}
|
|
void ANetworkCharacter::m_TickAbilities(float DeltaSeconds)
|
|
{
|
|
for(auto it = m_abilityStates.CreateIterator(); it; ++it)
|
|
{
|
|
AAbilityState* state = it.Value();
|
|
if((state->onCooldownTimer -= (DeltaSeconds * (1.0f + m_cooldownReduction))) <= 0.0f)
|
|
{
|
|
state->onCooldownTimer = 0.0f;
|
|
}
|
|
state->cooldownRate = state->onCooldownTimer / it.Key()->cooldown;
|
|
}
|
|
if(m_modifierManager)
|
|
m_modifierManager->Tick(DeltaSeconds);
|
|
m_timeSinceDamage += DeltaSeconds;
|
|
}
|
|
|
|
void ANetworkCharacter::m_SetAbilityCooldown(class UAbilityInfo* ability, class AAbilityState* state)
|
|
{
|
|
if(ability->abilityType == EAbilityType::Basic)
|
|
{
|
|
// This is the character's basic attack
|
|
state->onCooldownTimer = state->currentCooldownDuration = 1.0f / m_attackSpeed;
|
|
state->cooldownRate = (state->currentCooldownDuration > 0.0f) ? (state->onCooldownTimer / state->currentCooldownDuration) : 0.0f;
|
|
}
|
|
else
|
|
{
|
|
// This is an ability
|
|
state->NativeSetCooldown();
|
|
}
|
|
}
|
|
|
|
void ANetworkCharacter::m_OnAbilityCastConfirm_Server(UAbilityInfo* ability, bool success)
|
|
{
|
|
AAbilityState* state = GetAbilityState(ability);
|
|
m_OnAbilityCastConfirm(ability, success, state->toggleState, state->onCooldownTimer, state->currentCooldownDuration);
|
|
|
|
#if UE_INCLUDE_METRICS
|
|
if (metricsHandle && success && !ability->passive)
|
|
{
|
|
// Record ability cast
|
|
if (!ability->IsHoldOrToggle() || state->toggleState)
|
|
{
|
|
std::wstring abilityName = std::wstring() + ability->name;
|
|
if (abilityName.size() > 512)
|
|
abilityName.resize(512);
|
|
|
|
Metrics::RegisterAbility(ability->GetStaticHash(), abilityName);
|
|
metricsHandle->OnPlayerCast(ability->GetStaticHash());
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
void ANetworkCharacter::m_OnAbilityEventGroupEnded(class UAbilityInfo* ability, class AAbilityEventGroup* current, class AAbilityEventGroup* next)
|
|
{
|
|
check(Role == ROLE_Authority);
|
|
check(ability);
|
|
AAbilityEventGroup** group = m_activeAbilities.Find(ability);
|
|
check(group);
|
|
*group = next;
|
|
|
|
// Check end of channeling ability
|
|
if(m_channelGroup == current)
|
|
{
|
|
if(!next || !next->channelEvent)
|
|
{
|
|
//GWPRINT(L"End ability channel on ability " + m_castingAbility->GetName());
|
|
m_castingAbility = nullptr;
|
|
m_channelGroup = nullptr;
|
|
m_channelStun = false;
|
|
}
|
|
else
|
|
{
|
|
m_channelGroup = next;
|
|
m_channelStun = m_channelGroup->stunWhileChannel;
|
|
}
|
|
}
|
|
|
|
if(!next)
|
|
{
|
|
// Turn of toggle state if it was active
|
|
AAbilityState* state = GetAbilityState(ability);
|
|
if(state->toggleState == true)
|
|
{
|
|
state->toggleState = false;
|
|
m_SetAbilityCooldown(ability, state);
|
|
m_OnAbilityCastConfirm_Server(ability, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Subscribe to next group ended event
|
|
next->onAbilityEventGroupEnded.AddDynamic(this, &ANetworkCharacter::m_OnAbilityEventGroupEnded);
|
|
}
|
|
//GWPRINT(L"Ability " + ability->GetName() + L" reached next state " + (next ? next->GetName() : FString(L"end")));
|
|
}
|
|
|
|
void ANetworkCharacter::CastAbility(int32 abilityIndex)
|
|
{
|
|
if (abilityIndex < 0 || abilityIndex >= abilities.Num())
|
|
{
|
|
GWERROR(L"Ability index out of range");
|
|
return;
|
|
}
|
|
UAbilityInfo* ability = abilities[abilityIndex];
|
|
if (!ability)
|
|
{
|
|
GWERROR(L"Null ability assigned and casted on character character[" + GetName() + L"] slot[" + abilityIndex + L"]");
|
|
return;
|
|
}
|
|
CastAbility(ability);
|
|
}
|
|
void ANetworkCharacter::CastAbility(UAbilityInfo* ability)
|
|
{
|
|
if (!ability)
|
|
{
|
|
GWERROR(L"Ability pointer invalid");
|
|
return;
|
|
}
|
|
|
|
if(IsPendingKill() || IsPendingKillPending())
|
|
return;
|
|
|
|
AAbilityState* state = GetAbilityState(ability);
|
|
if(state->onCooldownTimer <= 0.0f && ability->precastEvent == nullptr)
|
|
{
|
|
m_CastAbility_Server(ability);
|
|
}
|
|
else if(ability->precastEvent != nullptr && state->onCooldownTimer <= 0.0f)
|
|
{
|
|
m_PreCast(ability);
|
|
}
|
|
}
|
|
|
|
class UAbilityInfo* ANetworkCharacter::GetCastingAbility()
|
|
{
|
|
return m_castingAbility;
|
|
}
|
|
|
|
AAbilityState* ANetworkCharacter::GetAbilityState(UAbilityInfo* abilityInfo)
|
|
{
|
|
AAbilityState** state = m_abilityStates.Find(abilityInfo);
|
|
if (!state)
|
|
{
|
|
// Don't check anymore if this character is allowed to cast an ability
|
|
// TODO: check this only for players in CastAbility_Validate when the new character class hierarchy is implemented
|
|
//check(abilities.Contains(abilityInfo) || passives.Contains(abilityInfo));
|
|
if (abilityInfo == nullptr)
|
|
{
|
|
FERROR("abilityInfo == nullptr");
|
|
return nullptr;
|
|
}
|
|
if(!abilityInfo->abilityState)
|
|
{
|
|
GWERROR(L"Ability " + abilityInfo->GetName() + L" does not have an ability state class assigned");
|
|
abilityInfo->abilityState = AAbilityState::StaticClass();
|
|
}
|
|
AAbilityState* abs = GetWorld()->SpawnActor<AAbilityState>(abilityInfo->abilityState);
|
|
abs->onCooldownTimer = 0.0f;
|
|
abs->cooldownRate = 0.0f;
|
|
abs->power = 0.0f;
|
|
abs->info = abilityInfo;
|
|
return m_abilityStates.Add(abilityInfo, abs);
|
|
}
|
|
return *state;
|
|
}
|
|
|
|
bool ANetworkCharacter::GetAbilityToggleState(class UAbilityInfo* abilityInfo)
|
|
{
|
|
AAbilityState* state = GetAbilityState(abilityInfo);
|
|
if(state)
|
|
{
|
|
return state->toggleState;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ANetworkCharacter::m_InteruptSpellcasting()
|
|
{
|
|
if (m_channelGroup)
|
|
{
|
|
m_channelGroup->Interrupt();
|
|
m_channelGroup = nullptr;
|
|
m_castingAbility = nullptr;
|
|
m_channelStun = false;
|
|
}
|
|
}
|
|
|
|
ModifierManager* ANetworkCharacter::GetModifierManager()
|
|
{
|
|
return m_modifierManager;
|
|
}
|
|
|
|
TArray<AModifier*> ANetworkCharacter::GetModifiersOfClass(TSubclassOf<class AModifier> modifierClass)
|
|
{
|
|
if (m_modifierManager)
|
|
return m_modifierManager->GetModifiersOfClass(modifierClass);
|
|
return TArray<AModifier*>();
|
|
}
|
|
void ANetworkCharacter::ResetModifiers()
|
|
{
|
|
m_blockedMana = 0;
|
|
m_attackDamageMultiplier = 1.0f;
|
|
m_magicDamageMultiplier = 1.0f;
|
|
m_damageMultiplier = 1.0f;
|
|
m_manaRegenMultiplier = 1.0f;
|
|
m_stunned = false;
|
|
m_stunned = false;
|
|
m_damageTakenMultiplier = 1.0f;
|
|
m_cooldownReduction = 0.0f;
|
|
m_ignoreArmor = 0.0f;
|
|
m_manaUsageMultiplier = 1.0f;
|
|
m_positiveEffectMultiplier = 1.0f;
|
|
m_negativeEffectMultiplier = 1.0f;
|
|
m_castingMovementspeedMultiplier = 0.0f;
|
|
m_armor = baseArmor;
|
|
GetCharacterMovement()->MaxWalkSpeed = baseMovementSpeed;
|
|
m_SetLevelStats();
|
|
}
|
|
|
|
void ANetworkCharacter::m_SetLevelStats()
|
|
{
|
|
|
|
}
|
|
|
|
void ANetworkCharacter::CheckStatsOverflow()
|
|
{
|
|
if (m_health > m_maxHealth || !m_initialised)
|
|
m_health = m_maxHealth;
|
|
if (m_mana > m_maxMana || !m_initialised)
|
|
m_mana = m_maxMana;
|
|
}
|
|
|
|
int32 ANetworkCharacter::DealDamage(UObject* WorldContextObject, int32 damage, float armorPercentageIgnore)
|
|
{
|
|
ADealDamageProxy* damageProxy = Cast<ADealDamageProxy>(WorldContextObject);
|
|
if(!damageProxy)
|
|
{
|
|
GWERROR(L"DealDamage can not be called from an object that does not inherit from ADealDamageProxy");
|
|
return 0;
|
|
}
|
|
|
|
int32 damageDealth = NativeDealDamage(damageProxy->character, damage, armorPercentageIgnore, damageProxy->abilityInfo);
|
|
|
|
return damageDealth;
|
|
}
|
|
int32 ANetworkCharacter::NativeDealDamage(class ANetworkCharacter* dealer, int32 damage, float armorPercentageIgnore, UAbilityInfo* ability)
|
|
{
|
|
// Ensure that this is only called on the server
|
|
check(Role == ROLE_Authority);
|
|
if(dealer == NULL || dealer->IsPendingKill())
|
|
{
|
|
FWARNING("character in dealdamage is null or pendingkill");
|
|
return 0;
|
|
}
|
|
|
|
|
|
// Scale the damage done or taken by creatures and players
|
|
if (dealer->IsA<ANetworkPlayer>() != this->IsA<ANetworkPlayer>())
|
|
{
|
|
ANetworkPlayer* const player = dealer->IsA<ANetworkPlayer>() ? Cast<ANetworkPlayer>(dealer) : Cast<ANetworkPlayer>(this);
|
|
if (IsValid(player))
|
|
{
|
|
ADefaultPlayerState* const state = Cast<ADefaultPlayerState>(player->PlayerState);
|
|
if (IsValid(state))
|
|
{
|
|
UCurveFloat* const curve = player == this ? player->creatureDamageDealtCurve : player->creatureDamageTakenCurve;
|
|
if (curve)
|
|
{
|
|
const float scalar = curve->GetFloatValue((float)state->GetLevel());
|
|
damage = int32((float)damage * (scalar < 0.0f ? 0.0f : scalar));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if(damage < 0)
|
|
{
|
|
GWERROR(L"Negative damage dealt by " + ability->GetName());
|
|
return 0;
|
|
}
|
|
const int32 damageCap = 1000;
|
|
if(damage > damageCap)
|
|
{
|
|
GWERROR(L"A lot of damage (>" + damageCap + L") dealt by " + ability->GetName() + ", is this intended?");
|
|
}
|
|
if(armorPercentageIgnore > 1.0f || armorPercentageIgnore < 0.0f)
|
|
{
|
|
GWERROR(L"Invalid armorPercentageIgnore given by " + ability->GetName() + "will use 0.0f");
|
|
armorPercentageIgnore = 0.0f;
|
|
// return 0;
|
|
}
|
|
|
|
float totalArmorPercentage = armorPercentageIgnore + dealer->m_ignoreArmor;
|
|
totalArmorPercentage = std::fmax(0.0f, std::fmin(totalArmorPercentage, 1.0f));
|
|
|
|
if(m_damageTakenMultiplier == 0.0f || dealer->m_damageMultiplier <= 0.0f)
|
|
return 0;
|
|
float divider = 1 + 0.01f * ((float)m_armor * (1 - totalArmorPercentage));
|
|
float damageToDeal;
|
|
// The damage to deal
|
|
damageToDeal = damage / divider;
|
|
|
|
damageToDeal *= dealer->m_damageMultiplier;
|
|
// Multiply by weakness aka damage taken multiplier
|
|
damageToDeal *= m_damageTakenMultiplier;
|
|
|
|
// Magic damage has a different multiplier
|
|
if (ability->abilityType == EAbilityType::Ability)
|
|
damageToDeal *= dealer->m_magicDamageMultiplier;
|
|
|
|
int32 iDamageToDeal = damageToDeal;
|
|
bool dealdamage = true;
|
|
for (AModifier* mod : m_modifierManager->m_modifiers)
|
|
{
|
|
if (!mod->OnDamage(iDamageToDeal, dealer))
|
|
{
|
|
dealdamage = false;
|
|
}
|
|
}
|
|
if (dealer->GetModifierManager() == nullptr)
|
|
return 0;
|
|
for (AModifier* mod : dealer->GetModifierManager()->m_modifiers)
|
|
{
|
|
if (!mod->OnDealDamage(iDamageToDeal, this))
|
|
{
|
|
dealdamage = false;
|
|
}
|
|
}
|
|
if(!dealdamage)
|
|
return 0;
|
|
|
|
#if UE_INCLUDE_METRICS
|
|
if (dealer->metricsHandle && IsValid(dealer))
|
|
{
|
|
// Record ability damage dealt
|
|
dealer->metricsHandle->OnPlayerDealDamage(ability->GetStaticHash(), iDamageToDeal);
|
|
}
|
|
#endif
|
|
|
|
if(iDamageToDeal > m_health)
|
|
iDamageToDeal = m_health;
|
|
RemoveHealth(iDamageToDeal);
|
|
m_lastPlayerDamage = dealer;
|
|
|
|
onDamageTaken.Broadcast(dealer, damage, ability);
|
|
|
|
// Update the entry for this team in the damage dealt table
|
|
const int32 team = dealer->GetTeam();
|
|
if (experienceGainOnKill > 0 && team < 5)
|
|
{
|
|
auto find = m_teamDamageDealt.find(team);
|
|
if (find != m_teamDamageDealt.end())
|
|
{
|
|
find->second.damage += iDamageToDeal;
|
|
find->second.timer = teamDamageTimer;
|
|
}
|
|
else
|
|
m_teamDamageDealt.emplace(team, DamagePeriod(iDamageToDeal, teamDamageTimer));
|
|
}
|
|
|
|
// Spawn combat text
|
|
ADefaultPlayerController* const controller = Cast<ADefaultPlayerController>(dealer->GetController());
|
|
if (IsValid(controller))
|
|
{
|
|
auto find = m_damageAccum.find(controller);
|
|
if (find == m_damageAccum.end())
|
|
m_damageAccum.emplace(controller, DamagePeriod(iDamageToDeal, damageAccumTimer));
|
|
else
|
|
find->second.damage += iDamageToDeal;
|
|
}
|
|
|
|
for (AModifier* mod : m_modifierManager->m_modifiers)
|
|
mod->AfterDamage(iDamageToDeal);
|
|
|
|
m_timeSinceDamage = 0;
|
|
|
|
if(m_health <= 0)
|
|
{
|
|
NativeOnKilled(dealer, ability);
|
|
OnDeath(ability);
|
|
}
|
|
return iDamageToDeal;
|
|
}
|
|
|
|
void ANetworkCharacter::NativeOnKilled(class ANetworkCharacter* killer, class UAbilityInfo* ability)
|
|
{
|
|
// Score counting
|
|
ANetworkPlayer* const thisPlayer = Cast<ANetworkPlayer>(this);
|
|
if (IsValid(thisPlayer))
|
|
{
|
|
ADefaultPlayerState* state = Cast<ADefaultPlayerState>(thisPlayer->PlayerState);
|
|
if (IsValid(state)) state->deaths++;
|
|
|
|
ANetworkPlayer* const killerPlayer = Cast<ANetworkPlayer>(killer);
|
|
if (IsValid(killerPlayer))
|
|
{
|
|
state = Cast<ADefaultPlayerState>(killerPlayer->PlayerState);
|
|
if (IsValid(state)) state->kills++;
|
|
}
|
|
}
|
|
|
|
if (experienceGainOnKill > 0 && !m_teamDamageDealt.empty())
|
|
{
|
|
ADefaultGameState* gameState = Cast<ADefaultGameState>(GetWorld()->GetGameState());
|
|
TArray<ADefaultPlayerState*> players = gameState->GetPlayers<ADefaultPlayerState>();
|
|
|
|
// Fetch highest team
|
|
int32 highestTeam = -1;
|
|
int32 highestDamage = 0;
|
|
for (auto i : m_teamDamageDealt)
|
|
{
|
|
if (i.second.damage > highestDamage)
|
|
{
|
|
highestDamage = i.second.damage;
|
|
highestTeam = i.first;
|
|
}
|
|
}
|
|
m_teamDamageDealt.clear();
|
|
|
|
// Apply experience
|
|
if (highestTeam > 0)
|
|
{
|
|
for (size_t i = 0; i < players.Num(); i++)
|
|
{
|
|
if (!players[i])
|
|
continue;
|
|
|
|
// Award the experience
|
|
if(players[i]->GetTeam() == highestTeam)
|
|
{
|
|
players[i]->GainExperience(experienceGainOnKill);
|
|
|
|
// Spawn EXP gain effect
|
|
FTransform t;
|
|
if(players[i]->character)
|
|
{
|
|
t.SetTranslation(players[i]->character->GetActorLocation());
|
|
ATargetedEffect* fx = GetWorld()->SpawnActorDeferred<ATargetedEffect>(expGainEffect, t);
|
|
fx->Init(0.0f, players[i]->character);
|
|
fx->target = GetActorLocation();
|
|
UGameplayStatics::FinishSpawningActor(fx, t);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear the damage accum table
|
|
if (!m_damageAccum.empty())
|
|
{
|
|
for (auto iter = m_damageAccum.begin(); iter != m_damageAccum.end();)
|
|
{
|
|
// Controller no longer exists
|
|
if (IsValid(iter->first))
|
|
{
|
|
DamagePeriod& value = iter->second;
|
|
|
|
if (value.damage > 0)
|
|
iter->first->SpawnArcingCombatText(GetActorLocation(), FString::FromInt(value.damage), FLinearColor::White);
|
|
}
|
|
|
|
iter = m_damageAccum.erase(iter);
|
|
}
|
|
}
|
|
|
|
#if UE_INCLUDE_METRICS
|
|
if(metricsHandle && IsValid(killer))
|
|
{
|
|
// Record player death
|
|
if(killer->metricsHandle)
|
|
metricsHandle->OnPlayerDie(killer->metricsHandle->Id());
|
|
else
|
|
metricsHandle->OnPlayerDie(0);
|
|
}
|
|
#endif
|
|
|
|
onCharacterKilled.Broadcast(killer, ability);
|
|
}
|
|
|
|
void ANetworkCharacter::OnStandardAttack(ANetworkCharacter* targetCharacter)
|
|
{
|
|
if (m_modifierManager==nullptr)
|
|
{
|
|
FWARNING("m_modifierManager==nullptr");
|
|
return;
|
|
}
|
|
for (AModifier* mod : m_modifierManager->m_modifiers)
|
|
{
|
|
if(IsValid(mod))
|
|
mod->OnStandardAttack(targetCharacter);
|
|
}
|
|
}
|
|
void ANetworkCharacter::AddHealth(int32 health)
|
|
{
|
|
NativeAddHealth(health);
|
|
}
|
|
void ANetworkCharacter::NativeAddHealth(int32 health)
|
|
{
|
|
check(Role == ROLE_Authority); // server call only
|
|
check(health >= 0);
|
|
|
|
if (m_health < m_maxHealth)
|
|
{
|
|
// Spawn combat text
|
|
ADefaultPlayerController* const controller = Cast<ADefaultPlayerController>(GetController());
|
|
if (IsValid(controller))
|
|
{
|
|
m_healAccumValue += health;
|
|
if (m_healAccumTimer <= 0.0f) m_healAccumTimer += healthAccumTimer;
|
|
}
|
|
|
|
m_health += health;
|
|
if (m_health > m_maxHealth)
|
|
m_health = m_maxHealth;
|
|
}
|
|
}
|
|
|
|
void ANetworkCharacter::RemoveHealth(int32 health)
|
|
{
|
|
check(Role == ROLE_Authority); // Server only call
|
|
m_health -= health;
|
|
}
|
|
void ANetworkCharacter::AddMana(float mana)
|
|
{
|
|
check(Role == ROLE_Authority); // Server only call
|
|
m_mana += mana * m_manaRegenMultiplier;
|
|
if (m_mana > (m_maxMana-m_blockedMana))
|
|
m_mana = (m_maxMana - m_blockedMana);
|
|
}
|
|
void ANetworkCharacter::RemoveMana(float mana)
|
|
{
|
|
check(Role == ROLE_Authority); // Server only call
|
|
if(mana > m_mana)
|
|
{
|
|
mana = m_mana; // mana has to remain positive or 0
|
|
}
|
|
m_mana -= mana;
|
|
}
|
|
int32 ANetworkCharacter::GetMana() const
|
|
{
|
|
return m_mana;
|
|
}
|
|
int32 ANetworkCharacter::GetBlockedMana() const
|
|
{
|
|
return m_blockedMana;
|
|
}
|
|
int32 ANetworkCharacter::GetMaxMana() const
|
|
{
|
|
return m_maxMana;
|
|
}
|
|
int32 ANetworkCharacter::GetHealth() const
|
|
{
|
|
return m_health;
|
|
}
|
|
int32 ANetworkCharacter::GetMaxHealth() const
|
|
{
|
|
return m_maxHealth;
|
|
}
|
|
int32 ANetworkCharacter::GetArmor() const
|
|
{
|
|
return m_armor;
|
|
}
|
|
float ANetworkCharacter::GetDamageMultiplier() const
|
|
{
|
|
return m_damageMultiplier;
|
|
}
|
|
float ANetworkCharacter::GetCooldownReduction() const
|
|
{
|
|
return m_cooldownReduction;
|
|
}
|
|
float ANetworkCharacter::GetAttackSpeed() const
|
|
{
|
|
return m_attackSpeed;
|
|
}
|
|
float ANetworkCharacter::GetAttackDamage() const
|
|
{
|
|
return m_attackDamageMultiplier;
|
|
}
|
|
float ANetworkCharacter::GetMagicDamage() const
|
|
{
|
|
return m_magicDamageMultiplier;
|
|
}
|
|
float ANetworkCharacter::GetTotalDamage() const
|
|
{
|
|
return m_totalDamage;
|
|
}
|
|
bool ANetworkCharacter::GetHitable() const
|
|
{
|
|
return m_hitable;
|
|
}
|
|
void ANetworkCharacter::SetHitable(bool hittable)
|
|
{
|
|
m_hitable = hittable;
|
|
}
|
|
bool ANetworkCharacter::IsStunned() const
|
|
{
|
|
return m_stunned || m_channelStun;
|
|
}
|
|
bool ANetworkCharacter::CanRotate() const
|
|
{
|
|
if(m_stunned)
|
|
return false;
|
|
if(m_channelStun && !m_allowChannelRotation)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool ANetworkCharacter::IsSilenced() const
|
|
{
|
|
return m_silenced;
|
|
}
|
|
bool ANetworkCharacter::IsChanneling() const
|
|
{
|
|
return m_castingAbility != nullptr;
|
|
}
|
|
bool ANetworkCharacter::IsRanged() const
|
|
{
|
|
return m_isRanged;
|
|
}
|
|
ANetworkCharacter* ANetworkCharacter::GetLastPlayerDamage() const
|
|
{
|
|
return IsValid(m_lastPlayerDamage) ? m_lastPlayerDamage : nullptr;
|
|
}
|
|
|
|
bool ANetworkCharacter::CanBeStunned() const
|
|
{
|
|
return canBeStunned;
|
|
}
|
|
|
|
float ANetworkCharacter::GetTimeSinceDamage() const
|
|
{
|
|
return m_timeSinceDamage;
|
|
}
|
|
|
|
bool ANetworkCharacter::IsVisible() const
|
|
{
|
|
return m_visible;
|
|
} |