693 lines
13 KiB
C++
693 lines
13 KiB
C++
// Copyright 2016 Mookie. All Rights Reserved.
|
|
|
|
#include "EBGun.h"
|
|
#include "EBMagazine.h"
|
|
#include "EBWeaponConfiguration.h"
|
|
#include "EBBullet.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
#include "Engine/Engine.h"
|
|
#include "TimerManager.h"
|
|
|
|
AEBGun::AEBGun()
|
|
{
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
bReplicates = true;
|
|
SetReplicateMovement(true);
|
|
|
|
// Create the gun mesh component
|
|
GunMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("GunMesh"));
|
|
RootComponent = GunMesh;
|
|
|
|
// Create the barrel component
|
|
Barrel = CreateDefaultSubobject<UEBBarrel>(TEXT("Barrel"));
|
|
Barrel->SetupAttachment(GunMesh, TEXT("MuzzleSocket"));
|
|
Barrel->SetParentGun(this);
|
|
|
|
// Create the magazine component
|
|
Magazine = CreateDefaultSubobject<UEBMagazine>(TEXT("Magazine"));
|
|
|
|
// Initialize state
|
|
CurrentGunState = EGunState::GS_Ready;
|
|
PreviousGunState = EGunState::GS_Ready;
|
|
bSafetyOn = false;
|
|
bChamberedRound = false;
|
|
bHammerCocked = false;
|
|
bTriggerPressed = false;
|
|
bCanProcessNextShot = true;
|
|
BurstShotsFired = 0;
|
|
ChamberedBulletType = nullptr;
|
|
}
|
|
|
|
void AEBGun::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
// Apply weapon configuration if available
|
|
if (WeaponConfig)
|
|
{
|
|
ApplyWeaponConfiguration(WeaponConfig);
|
|
}
|
|
|
|
// Initialize magazine
|
|
if (Magazine)
|
|
{
|
|
Magazine->OnMagazineEmpty.AddDynamic(this, &AEBGun::OnMagazineEmptyHandler);
|
|
Magazine->OnBulletFed.AddDynamic(this, &AEBGun::OnBulletFedHandler);
|
|
}
|
|
|
|
// Initialize barrel
|
|
if (Barrel)
|
|
{
|
|
Barrel->ShotFired.AddDynamic(this, &AEBGun::OnBarrelShotFiredHandler);
|
|
}
|
|
}
|
|
|
|
void AEBGun::Tick(float DeltaTime)
|
|
{
|
|
Super::Tick(DeltaTime);
|
|
|
|
// Process firing state
|
|
if (CurrentGunState == EGunState::GS_Firing)
|
|
{
|
|
ProcessFiring();
|
|
}
|
|
}
|
|
|
|
void AEBGun::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
|
|
DOREPLIFETIME(AEBGun, CurrentGunState);
|
|
DOREPLIFETIME(AEBGun, bSafetyOn);
|
|
DOREPLIFETIME(AEBGun, bChamberedRound);
|
|
DOREPLIFETIME(AEBGun, bHammerCocked);
|
|
DOREPLIFETIME(AEBGun, ChamberedBulletType);
|
|
}
|
|
|
|
void AEBGun::PullTrigger()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
ServerPullTrigger();
|
|
}
|
|
else
|
|
{
|
|
ServerPullTrigger();
|
|
}
|
|
}
|
|
|
|
void AEBGun::ServerPullTrigger_Implementation()
|
|
{
|
|
if (!CanFire())
|
|
{
|
|
return;
|
|
}
|
|
|
|
bTriggerPressed = true;
|
|
|
|
if (CurrentGunState == EGunState::GS_Ready)
|
|
{
|
|
SetGunState(EGunState::GS_Firing);
|
|
}
|
|
}
|
|
|
|
void AEBGun::ReleaseTrigger()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
ServerReleaseTrigger();
|
|
}
|
|
else
|
|
{
|
|
ServerReleaseTrigger();
|
|
}
|
|
}
|
|
|
|
void AEBGun::ServerReleaseTrigger_Implementation()
|
|
{
|
|
bTriggerPressed = false;
|
|
|
|
// Handle fire mode specific behavior
|
|
if (Barrel && WeaponConfig)
|
|
{
|
|
EFireMode CurrentFireMode = Barrel->FireMode;
|
|
if (CurrentFireMode == EFireMode::FM_Semiauto || CurrentFireMode == EFireMode::FM_Manual)
|
|
{
|
|
if (CurrentGunState == EGunState::GS_Firing)
|
|
{
|
|
SetGunState(EGunState::GS_Ready);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AEBGun::SetSafety(bool bNewSafetyState)
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
ServerSetSafety(bNewSafetyState);
|
|
}
|
|
else
|
|
{
|
|
ServerSetSafety(bNewSafetyState);
|
|
}
|
|
}
|
|
|
|
void AEBGun::ServerSetSafety_Implementation(bool bNewSafetyState)
|
|
{
|
|
if (!WeaponConfig || !WeaponConfig->FireControlConfig.bHasSafety)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool bOldSafetyState = bSafetyOn;
|
|
bSafetyOn = bNewSafetyState;
|
|
|
|
if (bSafetyOn && CurrentGunState == EGunState::GS_Firing)
|
|
{
|
|
SetGunState(EGunState::GS_SafetyOn);
|
|
}
|
|
else if (!bSafetyOn && CurrentGunState == EGunState::GS_SafetyOn)
|
|
{
|
|
SetGunState(EGunState::GS_Ready);
|
|
}
|
|
|
|
if (bOldSafetyState != bSafetyOn)
|
|
{
|
|
OnSafetyChanged.Broadcast(bSafetyOn);
|
|
MulticastOnSafetyChanged(bSafetyOn);
|
|
}
|
|
}
|
|
|
|
void AEBGun::CockHammer()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
bHammerCocked = true;
|
|
}
|
|
}
|
|
|
|
void AEBGun::ReleaseHammer()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
bHammerCocked = false;
|
|
}
|
|
}
|
|
|
|
void AEBGun::LoadMagazine(TArray<UEBBulletPropertiesAsset*> BulletTypes)
|
|
{
|
|
if (Magazine)
|
|
{
|
|
Magazine->LoadBullets(BulletTypes);
|
|
}
|
|
}
|
|
|
|
void AEBGun::EjectMagazine()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
ServerEjectMagazine();
|
|
}
|
|
else
|
|
{
|
|
ServerEjectMagazine();
|
|
}
|
|
}
|
|
|
|
void AEBGun::ServerEjectMagazine_Implementation()
|
|
{
|
|
if (Magazine)
|
|
{
|
|
Magazine->UnloadAllBullets();
|
|
}
|
|
|
|
OnMagazineChanged.Broadcast(nullptr);
|
|
MulticastOnMagazineChanged(nullptr);
|
|
}
|
|
|
|
void AEBGun::InsertMagazine(UEBMagazine* NewMagazine)
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
ServerInsertMagazine(NewMagazine);
|
|
}
|
|
else
|
|
{
|
|
ServerInsertMagazine(NewMagazine);
|
|
}
|
|
}
|
|
|
|
void AEBGun::ServerInsertMagazine_Implementation(UEBMagazine* NewMagazine)
|
|
{
|
|
if (!NewMagazine)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Replace magazine component (in a real implementation, this would be more complex)
|
|
Magazine = NewMagazine;
|
|
|
|
OnMagazineChanged.Broadcast(NewMagazine);
|
|
MulticastOnMagazineChanged(NewMagazine);
|
|
}
|
|
|
|
void AEBGun::ChargingHandle()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
ServerChargingHandle();
|
|
}
|
|
else
|
|
{
|
|
ServerChargingHandle();
|
|
}
|
|
}
|
|
|
|
void AEBGun::ServerChargingHandle_Implementation()
|
|
{
|
|
// Eject chambered round if present
|
|
if (bChamberedRound)
|
|
{
|
|
ChamberedBulletType = nullptr;
|
|
bChamberedRound = false;
|
|
}
|
|
|
|
// Feed new round from magazine
|
|
if (Magazine && Magazine->CanFeed())
|
|
{
|
|
UEBBulletPropertiesAsset* NewBullet = Magazine->FeedNextBullet();
|
|
if (NewBullet)
|
|
{
|
|
ChamberedBulletType = NewBullet;
|
|
bChamberedRound = true;
|
|
}
|
|
}
|
|
|
|
// Update gun state
|
|
if (CurrentGunState == EGunState::GS_Empty && bChamberedRound)
|
|
{
|
|
SetGunState(EGunState::GS_Ready);
|
|
}
|
|
else if (!bChamberedRound && (!Magazine || Magazine->bIsEmpty))
|
|
{
|
|
SetGunState(EGunState::GS_Empty);
|
|
}
|
|
}
|
|
|
|
void AEBGun::EjectChamberedRound()
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
if (bChamberedRound)
|
|
{
|
|
ChamberedBulletType = nullptr;
|
|
bChamberedRound = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AEBGun::CanFire() const
|
|
{
|
|
if (bSafetyOn)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (CurrentGunState == EGunState::GS_Jammed ||
|
|
CurrentGunState == EGunState::GS_Empty ||
|
|
CurrentGunState == EGunState::GS_SafetyOn ||
|
|
CurrentGunState == EGunState::GS_Malfunction)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!bChamberedRound || !ChamberedBulletType)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AEBGun::IsLoaded() const
|
|
{
|
|
return bChamberedRound && ChamberedBulletType != nullptr;
|
|
}
|
|
|
|
int32 AEBGun::GetRoundsInMagazine() const
|
|
{
|
|
if (Magazine)
|
|
{
|
|
return Magazine->GetCurrentRoundCount();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int32 AEBGun::GetTotalRounds() const
|
|
{
|
|
int32 TotalRounds = 0;
|
|
|
|
if (Magazine)
|
|
{
|
|
TotalRounds += Magazine->GetCurrentRoundCount();
|
|
}
|
|
|
|
if (bChamberedRound)
|
|
{
|
|
TotalRounds += 1;
|
|
}
|
|
|
|
return TotalRounds;
|
|
}
|
|
|
|
UEBBulletPropertiesAsset* AEBGun::GetChamberedBulletType() const
|
|
{
|
|
return ChamberedBulletType;
|
|
}
|
|
|
|
void AEBGun::ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig)
|
|
{
|
|
if (!NewConfig)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WeaponConfig = NewConfig;
|
|
|
|
// Apply configuration to barrel
|
|
if (Barrel)
|
|
{
|
|
// Set parent gun reference
|
|
Barrel->SetParentGun(this);
|
|
|
|
// Apply barrel-specific configuration
|
|
Barrel->ApplyBarrelConfiguration(NewConfig->BarrelConfig);
|
|
|
|
// Apply fire control configuration
|
|
Barrel->FireMode = NewConfig->FireControlConfig.DefaultFireMode;
|
|
Barrel->FireRateMin = NewConfig->FireControlConfig.FireRateMin / 60.0f; // Convert RPM to RPS
|
|
Barrel->FireRateMax = NewConfig->FireControlConfig.FireRateMax / 60.0f;
|
|
Barrel->BurstCount = NewConfig->FireControlConfig.BurstCount;
|
|
Barrel->BurstCooldown = NewConfig->FireControlConfig.BurstCooldown;
|
|
Barrel->GatlingSpoolUpTime = NewConfig->FireControlConfig.GatlingSpoolUpTime;
|
|
Barrel->GatlingSpoolDownTime = NewConfig->FireControlConfig.GatlingSpoolDownTime;
|
|
Barrel->GatlingAutoSpool = NewConfig->FireControlConfig.bGatlingAutoSpool;
|
|
|
|
// Apply networking configuration
|
|
Barrel->ReplicateVariables = NewConfig->bReplicateVariables;
|
|
Barrel->ReplicateShotFiredEvents = NewConfig->bReplicateShotEvents;
|
|
Barrel->ClientSideAim = NewConfig->bClientSideAim;
|
|
|
|
// Disable legacy ammo system in favor of gun's magazine system
|
|
Barrel->CycleAmmo = false;
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("Gun '%s' applied configuration to barrel"), *NewConfig->GetWeaponDisplayName());
|
|
}
|
|
|
|
// Apply configuration to magazine
|
|
if (Magazine)
|
|
{
|
|
Magazine->MagazineConfig.MaxCapacity = NewConfig->DefaultMagazineCapacity;
|
|
Magazine->MagazineConfig.MagazineType = NewConfig->DefaultMagazineType;
|
|
Magazine->MagazineConfig.bAllowMixedBulletTypes = NewConfig->bAllowMixedAmmo;
|
|
Magazine->MagazineConfig.MalfunctionChance = NewConfig->ReliabilityConfig.MalfunctionRate;
|
|
}
|
|
|
|
// Set default safety state
|
|
if (NewConfig->FireControlConfig.bHasSafety)
|
|
{
|
|
bSafetyOn = NewConfig->FireControlConfig.bDefaultSafetyOn;
|
|
}
|
|
else
|
|
{
|
|
bSafetyOn = false;
|
|
}
|
|
}
|
|
|
|
void AEBGun::SetFireMode(EFireMode NewFireMode)
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
ServerSetFireMode(NewFireMode);
|
|
}
|
|
else
|
|
{
|
|
ServerSetFireMode(NewFireMode);
|
|
}
|
|
}
|
|
|
|
void AEBGun::ServerSetFireMode_Implementation(EFireMode NewFireMode)
|
|
{
|
|
if (!WeaponConfig || !WeaponConfig->IsFireModeAvailable(NewFireMode))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Barrel)
|
|
{
|
|
Barrel->FireMode = NewFireMode;
|
|
}
|
|
}
|
|
|
|
EFireMode AEBGun::GetFireMode() const
|
|
{
|
|
if (Barrel)
|
|
{
|
|
return Barrel->FireMode;
|
|
}
|
|
|
|
return EFireMode::FM_Semiauto;
|
|
}
|
|
|
|
void AEBGun::SetGunState(EGunState NewState)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (CurrentGunState == NewState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PreviousGunState = CurrentGunState;
|
|
CurrentGunState = NewState;
|
|
|
|
OnGunStateChanged.Broadcast(NewState);
|
|
OnGunStateChangedInternal(PreviousGunState, NewState);
|
|
}
|
|
|
|
void AEBGun::OnRep_GunState()
|
|
{
|
|
OnGunStateChanged.Broadcast(CurrentGunState);
|
|
OnGunStateChangedInternal(PreviousGunState, CurrentGunState);
|
|
}
|
|
|
|
void AEBGun::ProcessFiring()
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanFire() || !bCanProcessNextShot)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check if we should continue firing based on fire mode
|
|
if (Barrel && WeaponConfig)
|
|
{
|
|
EFireMode CurrentFireMode = Barrel->FireMode;
|
|
|
|
switch (CurrentFireMode)
|
|
{
|
|
case EFireMode::FM_Semiauto:
|
|
case EFireMode::FM_Manual:
|
|
if (!bTriggerPressed)
|
|
{
|
|
SetGunState(EGunState::GS_Ready);
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case EFireMode::FM_Auto:
|
|
if (!bTriggerPressed)
|
|
{
|
|
SetGunState(EGunState::GS_Ready);
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case EFireMode::FM_Burst:
|
|
case EFireMode::FM_InterBurst:
|
|
if (BurstShotsFired >= WeaponConfig->FireControlConfig.BurstCount)
|
|
{
|
|
BurstShotsFired = 0;
|
|
SetGunState(EGunState::GS_Ready);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fire the weapon
|
|
FireShot();
|
|
}
|
|
|
|
void AEBGun::FireShot()
|
|
{
|
|
if (!HasAuthority() || !CanFire())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Fire through barrel
|
|
if (Barrel && ChamberedBulletType)
|
|
{
|
|
// Set bullet class on barrel (this is a simplified approach)
|
|
// In a real implementation, you would need to create a bullet and configure it properly
|
|
Barrel->Ammo.Empty();
|
|
if (ChamberedBulletType)
|
|
{
|
|
// For now, we'll use the default bullet class since we can't get the class from the asset
|
|
// This would need to be properly implemented with a bullet class reference in the asset
|
|
}
|
|
|
|
// Fire the shot
|
|
Barrel->Shoot(true);
|
|
|
|
// Clear chambered round
|
|
ChamberedBulletType = nullptr;
|
|
bChamberedRound = false;
|
|
|
|
// Update burst count
|
|
BurstShotsFired++;
|
|
|
|
// Set fire rate cooldown
|
|
float FireRate = WeaponConfig ? WeaponConfig->GetEffectiveFireRate(GetFireMode()) : 600.0f;
|
|
float CooldownTime = 60.0f / FireRate; // Convert RPM to seconds
|
|
|
|
bCanProcessNextShot = false;
|
|
GetWorld()->GetTimerManager().SetTimer(FireRateTimer, [this]()
|
|
{
|
|
bCanProcessNextShot = true;
|
|
}, CooldownTime, false);
|
|
|
|
// Try to feed next round
|
|
if (!FeedNextRound())
|
|
{
|
|
SetGunState(EGunState::GS_Empty);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AEBGun::FeedNextRound()
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Magazine && Magazine->CanFeed())
|
|
{
|
|
UEBBulletPropertiesAsset* NextBullet = Magazine->FeedNextBullet();
|
|
if (NextBullet)
|
|
{
|
|
ChamberedBulletType = NextBullet;
|
|
bChamberedRound = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void AEBGun::OnMagazineEmptyHandler(UEBMagazine* EmptyMagazine)
|
|
{
|
|
OnMagazineEmpty.Broadcast();
|
|
|
|
if (!bChamberedRound)
|
|
{
|
|
SetGunState(EGunState::GS_Empty);
|
|
}
|
|
}
|
|
|
|
void AEBGun::OnBulletFedHandler(UEBMagazine* SourceMagazine, UEBBulletPropertiesAsset* BulletType)
|
|
{
|
|
// Handle bullet fed from magazine
|
|
}
|
|
|
|
void AEBGun::OnBarrelShotFiredHandler()
|
|
{
|
|
OnGunFired.Broadcast(ChamberedBulletType);
|
|
MulticastOnGunFired(ChamberedBulletType);
|
|
}
|
|
|
|
void AEBGun::MulticastOnGunFired_Implementation(UEBBulletPropertiesAsset* BulletType)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
OnGunFired.Broadcast(BulletType);
|
|
}
|
|
}
|
|
|
|
void AEBGun::MulticastOnMagazineChanged_Implementation(UEBMagazine* NewMagazine)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
OnMagazineChanged.Broadcast(NewMagazine);
|
|
}
|
|
}
|
|
|
|
void AEBGun::MulticastOnSafetyChanged_Implementation(bool bNewSafetyState)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
OnSafetyChanged.Broadcast(bNewSafetyState);
|
|
}
|
|
}
|
|
|
|
// Barrel Integration Functions
|
|
float AEBGun::GetBarrelLength() const
|
|
{
|
|
if (Barrel)
|
|
{
|
|
return Barrel->BarrelLength;
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
float AEBGun::GetEffectiveMuzzleVelocity() const
|
|
{
|
|
if (Barrel && ChamberedBulletType)
|
|
{
|
|
return Barrel->CalculateEffectiveMuzzleVelocity(ChamberedBulletType);
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
float AEBGun::GetBarrelAccuracy() const
|
|
{
|
|
if (Barrel)
|
|
{
|
|
return Barrel->CalculateBarrelAccuracy();
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
FVector AEBGun::CalculateRecoilImpulse() const
|
|
{
|
|
if (Barrel && ChamberedBulletType)
|
|
{
|
|
return Barrel->CalculateRecoilImpulse(ChamberedBulletType);
|
|
}
|
|
return FVector::ZeroVector;
|
|
}
|
|
|
|
bool AEBGun::IsBarrelConnected() const
|
|
{
|
|
return Barrel != nullptr && Barrel->IsConnectedToGun();
|
|
}
|