983 lines
22 KiB
C++
983 lines
22 KiB
C++
// Copyright 2016 Mookie. All Rights Reserved.
|
||
|
||
#include "EBGun.h"
|
||
#include "EBMagazine.h"
|
||
#include "EBWeaponData.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 a scene component as root to hold both mesh components
|
||
USceneComponent* RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
|
||
RootComponent = RootScene;
|
||
|
||
// Create both mesh components and attach to root
|
||
SkeletalGunMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalGunMesh"));
|
||
StaticGunMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticGunMesh"));
|
||
|
||
SkeletalGunMesh->SetupAttachment(RootComponent);
|
||
StaticGunMesh->SetupAttachment(RootComponent);
|
||
|
||
// Set skeletal mesh as default active component
|
||
StaticGunMesh->SetVisibility(false);
|
||
StaticGunMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||
|
||
// Create the barrel component for backward compatibility
|
||
Barrel = CreateDefaultSubobject<UEBBarrel>(TEXT("Barrel"));
|
||
Barrel->SetupAttachment(SkeletalGunMesh, 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;
|
||
|
||
// Initialize firing state
|
||
bShooting = false;
|
||
bSpooling = false;
|
||
GatlingRPS = 0.0f;
|
||
Cooldown = 0.0f;
|
||
BurstRemaining = 0;
|
||
|
||
// Initialize gun configuration
|
||
bUseSkeletalMesh = true;
|
||
bUseSkeletal = true;
|
||
MuzzleSocketName = TEXT("MuzzleSocket");
|
||
MuzzleSocket = TEXT("MuzzleSocket");
|
||
MuzzleTransform = FTransform::Identity;
|
||
MuzzleOffset = FTransform::Identity;
|
||
SkeletalMeshAsset = nullptr;
|
||
StaticMeshAsset = nullptr;
|
||
|
||
// Initialize random stream
|
||
RandomStream.Initialize(FMath::Rand());
|
||
}
|
||
|
||
void AEBGun::BeginPlay()
|
||
{
|
||
Super::BeginPlay();
|
||
|
||
// Synchronize the actor settings with component settings
|
||
bUseSkeletalMesh = bUseSkeletal;
|
||
MuzzleSocketName = MuzzleSocket;
|
||
MuzzleTransform = MuzzleOffset;
|
||
|
||
// Apply mesh assets from actor settings
|
||
if (bUseSkeletal && SkeletalMeshAsset)
|
||
{
|
||
SkeletalGunMesh->SetSkeletalMesh(SkeletalMeshAsset);
|
||
}
|
||
else if (!bUseSkeletal && StaticMeshAsset)
|
||
{
|
||
StaticGunMesh->SetStaticMesh(StaticMeshAsset);
|
||
}
|
||
|
||
// Apply the mesh type setting to ensure proper visibility
|
||
SetMeshType(bUseSkeletal);
|
||
|
||
// Apply weapon configuration if available
|
||
if (WeaponData)
|
||
{
|
||
ApplyWeaponData(WeaponData);
|
||
}
|
||
|
||
// 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)
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - AEBGun::Tick::FIRE()"));
|
||
ProcessFiring();
|
||
}
|
||
}
|
||
|
||
#if WITH_EDITOR
|
||
void AEBGun::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||
{
|
||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||
|
||
if (PropertyChangedEvent.Property)
|
||
{
|
||
FName PropertyName = PropertyChangedEvent.Property->GetFName();
|
||
|
||
// Handle mesh setup changes
|
||
if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, bUseSkeletal))
|
||
{
|
||
// Synchronize with internal setting
|
||
bUseSkeletalMesh = bUseSkeletal;
|
||
SetMeshType(bUseSkeletalMesh);
|
||
}
|
||
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, SkeletalMeshAsset))
|
||
{
|
||
if (SkeletalMeshAsset && SkeletalGunMesh)
|
||
{
|
||
SkeletalGunMesh->SetSkeletalMesh(SkeletalMeshAsset);
|
||
}
|
||
}
|
||
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, StaticMeshAsset))
|
||
{
|
||
if (StaticMeshAsset && StaticGunMesh)
|
||
{
|
||
StaticGunMesh->SetStaticMesh(StaticMeshAsset);
|
||
}
|
||
}
|
||
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, MuzzleSocket))
|
||
{
|
||
MuzzleSocketName = MuzzleSocket;
|
||
}
|
||
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, MuzzleOffset))
|
||
{
|
||
MuzzleTransform = MuzzleOffset;
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
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);
|
||
DOREPLIFETIME(AEBGun, FireMode);
|
||
DOREPLIFETIME(AEBGun, bShooting);
|
||
DOREPLIFETIME(AEBGun, bSpooling);
|
||
}
|
||
|
||
void AEBGun::PullTrigger()
|
||
{
|
||
if (HasAuthority())
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - HasAuthority()"));
|
||
ServerPullTrigger();
|
||
}
|
||
else
|
||
{
|
||
ServerPullTrigger();
|
||
}
|
||
}
|
||
|
||
void AEBGun::ServerPullTrigger_Implementation()
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - ServerPullTrigger_Implementation()"));
|
||
if (!CanFire())
|
||
{
|
||
return;
|
||
}
|
||
|
||
bTriggerPressed = true;
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - bTriggerPressed"));
|
||
if (CurrentGunState == EGunState::GS_Ready)
|
||
{
|
||
SetGunState(EGunState::GS_Firing);
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - GS_Firing"));
|
||
}
|
||
}
|
||
|
||
void AEBGun::ReleaseTrigger()
|
||
{
|
||
if (HasAuthority())
|
||
{
|
||
ServerReleaseTrigger();
|
||
}
|
||
else
|
||
{
|
||
ServerReleaseTrigger();
|
||
}
|
||
}
|
||
|
||
void AEBGun::ServerReleaseTrigger_Implementation()
|
||
{
|
||
bTriggerPressed = false;
|
||
|
||
// Handle fire mode specific behavior
|
||
if (FireMode == EFireMode::FM_Semiauto || FireMode == 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 (!WeaponData || !WeaponData->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)
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() - Safety is ON"));
|
||
return false;
|
||
}
|
||
|
||
if (CurrentGunState == EGunState::GS_Jammed ||
|
||
CurrentGunState == EGunState::GS_Empty ||
|
||
CurrentGunState == EGunState::GS_SafetyOn ||
|
||
CurrentGunState == EGunState::GS_Malfunction)
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() - Gun is not in a ready state: %s"), *UEnum::GetValueAsString(CurrentGunState));
|
||
return false;
|
||
}
|
||
|
||
if (!bChamberedRound || !ChamberedBulletType)
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() - No chambered round or bullet type"));
|
||
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::ApplyWeaponData(UEBWeaponData* NewData)
|
||
{
|
||
if (!NewData) return;
|
||
WeaponData = NewData;
|
||
|
||
// Apply basic properties
|
||
|
||
// Barrel & Ballistics (defaults for unmapped)
|
||
BarrelLength = 16.0f; // Default, no direct map
|
||
RiflingTwist = 7.0f; // Default
|
||
BoreRadius = 0.112f; // Default
|
||
MuzzleVelocityMin = NewData->MuzzleVelocity;
|
||
MuzzleVelocityMax = NewData->MuzzleVelocity;
|
||
Spread = 1.0f - NewData->Accuracy;
|
||
SpreadBias = 0.0f; // Default
|
||
MuzzleVelocityMultiplierMin = 1.0f;
|
||
MuzzleVelocityMultiplierMax = 1.0f;
|
||
|
||
// Fire Control
|
||
FireMode = NewData->DefaultFireMode;
|
||
FireRateMin = NewData->FireRate / 60.0f;
|
||
FireRateMax = NewData->FireRate / 60.0f;
|
||
BurstCount = NewData->BurstCount;
|
||
BurstCooldown = 0.0f; // Default
|
||
|
||
// Gatling (defaults)
|
||
bGatlingAutoSpool = true;
|
||
GatlingSpoolUpTime = 1.0f;
|
||
GatlingSpoolDownTime = 1.0f;
|
||
|
||
// Magazine
|
||
if (Magazine) {
|
||
Magazine->MagazineConfig.MaxCapacity = NewData->MagazineSize;
|
||
// Add other magazine configs if needed
|
||
}
|
||
|
||
// Safety
|
||
if (NewData->bHasSafety) {
|
||
// bSafetyOn is managed by the gun state, not set from data by default.
|
||
// Default is 'false' (off). Use SetSafety() to change.
|
||
} else {
|
||
bSafetyOn = false;
|
||
}
|
||
|
||
// Apply to barrel for compatibility
|
||
if (Barrel) {
|
||
// Map relevant fields to barrel
|
||
}
|
||
|
||
UE_LOG(LogTemp, Log, TEXT("Gun '%s' applied data configuration"), *NewData->WeaponName.ToString());
|
||
}
|
||
|
||
void AEBGun::SetFireMode(EFireMode NewFireMode)
|
||
{
|
||
if (HasAuthority())
|
||
{
|
||
ServerSetFireMode(NewFireMode);
|
||
}
|
||
else
|
||
{
|
||
ServerSetFireMode(NewFireMode);
|
||
}
|
||
}
|
||
|
||
void AEBGun::ServerSetFireMode_Implementation(EFireMode NewFireMode)
|
||
{
|
||
if (!WeaponData || !WeaponData->IsFireModeSupported(NewFireMode))
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] SetFireMode() FAILED - Invalid fire mode: %s"), *UEnum::GetValueAsString(NewFireMode));
|
||
//Is weapon config null?
|
||
if (!WeaponData)
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] Weapon config is null"));
|
||
}
|
||
//Is the fire mode available?
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] Fire mode available: %s"), WeaponData->IsFireModeSupported(NewFireMode) ? TEXT("True") : TEXT("False"));
|
||
return;
|
||
}
|
||
|
||
FireMode = NewFireMode;
|
||
|
||
// Update barrel for backward compatibility
|
||
// Barrel no longer keeps FireMode state.
|
||
}
|
||
|
||
EFireMode AEBGun::GetFireMode() const
|
||
{
|
||
return FireMode;
|
||
}
|
||
|
||
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())
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() FAILED - No Authority"));
|
||
return;
|
||
}
|
||
|
||
if (!CanFire() || !bCanProcessNextShot)
|
||
{
|
||
//Print CanFire and bCanProcessNextShot state
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - CanFire: %s, bCanProcessNextShot: %s"),
|
||
CanFire() ? TEXT("True") : TEXT("False"),
|
||
bCanProcessNextShot ? TEXT("True") : TEXT("False"));
|
||
return;
|
||
}
|
||
|
||
// Check if we should continue firing based on fire mode
|
||
switch (FireMode)
|
||
{
|
||
case EFireMode::FM_Semiauto:
|
||
case EFireMode::FM_Manual:
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() - FM_Semiauto or FM_Manual"));
|
||
if (!bTriggerPressed)
|
||
{
|
||
SetGunState(EGunState::GS_Ready);
|
||
return;
|
||
}
|
||
break;
|
||
|
||
case EFireMode::FM_Auto:
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() - FM_Auto"));
|
||
if (!bTriggerPressed)
|
||
{
|
||
SetGunState(EGunState::GS_Ready);
|
||
return;
|
||
}
|
||
break;
|
||
|
||
case EFireMode::FM_Burst:
|
||
case EFireMode::FM_InterBurst:
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() - FM_Burst or FM_InterBurst"));
|
||
if (BurstShotsFired >= BurstCount)
|
||
{
|
||
BurstShotsFired = 0;
|
||
SetGunState(EGunState::GS_Ready);
|
||
return;
|
||
}
|
||
break;
|
||
}
|
||
|
||
// Fire the weapon
|
||
FireShot();
|
||
}
|
||
|
||
void AEBGun::FireShot()
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] FireShot() called - Firing shot from AEBGun"));
|
||
if (!HasAuthority() || !CanFire())
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] FireShot() FAILED - CanFire() returned false or no authority"));
|
||
return;
|
||
}
|
||
|
||
// Fire through barrel
|
||
if (Barrel && ChamberedBulletType)
|
||
{
|
||
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] FireShot() - Firing shot from barrel"));
|
||
|
||
// Ensure we have a valid bullet class to spawn
|
||
if (!ChamberedBulletType->BulletClass)
|
||
{
|
||
UE_LOG(LogTemp, Error, TEXT("[WEAPON DEBUG] FireShot() FAILED: Bullet Properties Asset '%s' does not have a BulletClass assigned!"), *ChamberedBulletType->GetName());
|
||
return; // Stop if we don't have a bullet to spawn.
|
||
}
|
||
|
||
// Fire a single round directly through the barrel without relying on the legacy state machine
|
||
Barrel->FireBullet(ChamberedBulletType->BulletClass);
|
||
|
||
// Clear chambered round in the gun
|
||
ChamberedBulletType = nullptr;
|
||
bChamberedRound = false;
|
||
|
||
// Update burst count
|
||
BurstShotsFired++;
|
||
|
||
// Set fire-rate cooldown. Ensure the selected rate is positive so the timer actually triggers.
|
||
float ActualFireRate = RandomStream.FRandRange(FireRateMin, FireRateMax);
|
||
if (ActualFireRate <= KINDA_SMALL_NUMBER)
|
||
{
|
||
ActualFireRate = 1.0f; // fallback to 1 RPS (60 RPM)
|
||
}
|
||
float CooldownTime = 1.0f / ActualFireRate; // FireRate is already in RPS
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// Gun Properties Functions (moved from barrel)
|
||
float AEBGun::GetBarrelLength() const
|
||
{
|
||
return BarrelLength;
|
||
}
|
||
|
||
float AEBGun::GetEffectiveMuzzleVelocity() const
|
||
{
|
||
if (ChamberedBulletType)
|
||
{
|
||
// Calculate effective muzzle velocity based on gun properties
|
||
// This is a simplified calculation - in a real implementation you'd want
|
||
// to consider bullet properties, barrel length, powder charge, etc.
|
||
float BaseVelocity = FMath::RandRange(MuzzleVelocityMin, MuzzleVelocityMax);
|
||
float VelocityMultiplier = FMath::RandRange(MuzzleVelocityMultiplierMin, MuzzleVelocityMultiplierMax);
|
||
return BaseVelocity * VelocityMultiplier;
|
||
}
|
||
return 0.0f;
|
||
}
|
||
|
||
float AEBGun::GetBarrelAccuracy() const
|
||
{
|
||
// Calculate accuracy based on gun properties
|
||
// This is a simplified calculation
|
||
float AccuracyFactor = 1.0f - Spread;
|
||
return FMath::Clamp(AccuracyFactor, 0.0f, 1.0f);
|
||
}
|
||
|
||
FVector AEBGun::CalculateRecoilImpulse() const
|
||
{
|
||
if (ChamberedBulletType)
|
||
{
|
||
// Calculate recoil based on gun properties and bullet
|
||
// This is a simplified calculation - in reality you'd consider:
|
||
// - Bullet mass and velocity
|
||
// - Powder charge
|
||
// - Gun weight
|
||
// - Barrel length
|
||
float RecoilMagnitude = GetEffectiveMuzzleVelocity() * 0.001f; // Simple scaling
|
||
FVector RecoilDirection = -GetActorForwardVector();
|
||
return RecoilDirection * RecoilMagnitude;
|
||
}
|
||
return FVector::ZeroVector;
|
||
}
|
||
|
||
bool AEBGun::IsBarrelConnected() const
|
||
{
|
||
return Barrel != nullptr && Barrel->IsConnectedToGun();
|
||
}
|
||
|
||
// Gun Mesh Management Functions
|
||
void AEBGun::SetMeshType(bool bUseSkeletalMeshParam)
|
||
{
|
||
bUseSkeletalMesh = bUseSkeletalMeshParam;
|
||
|
||
if (bUseSkeletalMesh)
|
||
{
|
||
// Switch to skeletal mesh
|
||
SkeletalGunMesh->SetVisibility(true);
|
||
SkeletalGunMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||
StaticGunMesh->SetVisibility(false);
|
||
StaticGunMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||
|
||
// Reattach barrel to skeletal mesh
|
||
if (Barrel)
|
||
{
|
||
Barrel->SetupAttachment(SkeletalGunMesh, *MuzzleSocketName);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Switch to static mesh
|
||
StaticGunMesh->SetVisibility(true);
|
||
StaticGunMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||
SkeletalGunMesh->SetVisibility(false);
|
||
SkeletalGunMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||
|
||
// Reattach barrel to static mesh
|
||
if (Barrel)
|
||
{
|
||
Barrel->SetupAttachment(StaticGunMesh);
|
||
}
|
||
}
|
||
}
|
||
|
||
UPrimitiveComponent* AEBGun::GetActiveMeshComponent() const
|
||
{
|
||
return bUseSkeletalMesh ? Cast<UPrimitiveComponent>(SkeletalGunMesh) : Cast<UPrimitiveComponent>(StaticGunMesh);
|
||
}
|
||
|
||
FTransform AEBGun::GetMuzzleTransform() const
|
||
{
|
||
if (bUseSkeletalMesh && SkeletalGunMesh)
|
||
{
|
||
if (SkeletalGunMesh->DoesSocketExist(*MuzzleSocketName))
|
||
{
|
||
return SkeletalGunMesh->GetSocketTransform(*MuzzleSocketName);
|
||
}
|
||
}
|
||
else if (!bUseSkeletalMesh && StaticGunMesh)
|
||
{
|
||
// For static mesh, use the relative transform
|
||
return StaticGunMesh->GetComponentTransform() * MuzzleTransform;
|
||
}
|
||
|
||
// Fallback to component transform
|
||
UPrimitiveComponent* ActiveMesh = GetActiveMeshComponent();
|
||
return ActiveMesh ? ActiveMesh->GetComponentTransform() : GetActorTransform();
|
||
}
|
||
|
||
void AEBGun::SetMuzzleSocketName(const FString& NewSocketName)
|
||
{
|
||
MuzzleSocketName = NewSocketName;
|
||
|
||
// Update barrel attachment if using skeletal mesh
|
||
if (bUseSkeletalMesh && Barrel)
|
||
{
|
||
Barrel->SetupAttachment(SkeletalGunMesh, *MuzzleSocketName);
|
||
}
|
||
}
|
||
|
||
// Firing System Functions (moved from barrel)
|
||
void AEBGun::Shoot(bool bTrigger)
|
||
{
|
||
if (HasAuthority())
|
||
{
|
||
ServerShoot(bTrigger);
|
||
}
|
||
else
|
||
{
|
||
ServerShoot(bTrigger);
|
||
}
|
||
}
|
||
|
||
void AEBGun::ServerShoot_Implementation(bool bTrigger)
|
||
{
|
||
if (bTrigger)
|
||
{
|
||
PullTrigger();
|
||
}
|
||
else
|
||
{
|
||
ReleaseTrigger();
|
||
}
|
||
}
|
||
|
||
bool AEBGun::ServerShoot_Validate(bool bTrigger)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
void AEBGun::GatlingSpool(bool bSpool)
|
||
{
|
||
if (HasAuthority())
|
||
{
|
||
ServerGatlingSpool(bSpool);
|
||
}
|
||
else
|
||
{
|
||
ServerGatlingSpool(bSpool);
|
||
}
|
||
}
|
||
|
||
void AEBGun::ServerGatlingSpool_Implementation(bool bSpool)
|
||
{
|
||
bSpooling = bSpool;
|
||
}
|
||
|
||
bool AEBGun::ServerGatlingSpool_Validate(bool bSpool)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
void AEBGun::SwitchFireMode(EFireMode NewFireMode)
|
||
{
|
||
if (HasAuthority())
|
||
{
|
||
ServerSwitchFireMode(NewFireMode);
|
||
}
|
||
else
|
||
{
|
||
ServerSwitchFireMode(NewFireMode);
|
||
}
|
||
}
|
||
|
||
void AEBGun::ServerSwitchFireMode_Implementation(EFireMode NewFireMode)
|
||
{
|
||
if (WeaponData && WeaponData->IsFireModeSupported(NewFireMode)) {
|
||
FireMode = NewFireMode;
|
||
}
|
||
}
|
||
|
||
bool AEBGun::ServerSwitchFireMode_Validate(EFireMode NewFireMode)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Legacy SpawnBullet function removed – firing handled by FireShot / FireBullet.
|
||
|
||
void AEBGun::PredictHit(bool& bHit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime,
|
||
AActor*& HitActor, TArray<FVector>& Trajectory,
|
||
TSubclassOf<class AEBBullet> BulletClass,
|
||
const TArray<AActor*>& IgnoredActors,
|
||
float MaxTime, float Step) const
|
||
{
|
||
// For now, delegate to barrel for backward compatibility
|
||
if (Barrel)
|
||
{
|
||
Barrel->PredictHit(bHit, TraceResult, HitLocation, HitTime, HitActor, Trajectory, BulletClass, IgnoredActors, MaxTime, Step);
|
||
}
|
||
}
|
||
|
||
void AEBGun::CalculateAimDirection(TSubclassOf<class AEBBullet> BulletClass, FVector TargetLocation, FVector TargetVelocity,
|
||
FVector& AimDirection, FVector& PredictedTargetLocation, FVector& PredictedIntersectionLocation,
|
||
float& PredictedFlightTime, float& Error, float MaxTime, float Step, int32 NumIterations) const
|
||
{
|
||
// For now, delegate to barrel for backward compatibility
|
||
if (Barrel)
|
||
{
|
||
Barrel->CalculateAimDirection(BulletClass, TargetLocation, TargetVelocity, AimDirection, PredictedTargetLocation,
|
||
PredictedIntersectionLocation, PredictedFlightTime, Error, MaxTime, Step, NumIterations);
|
||
}
|
||
}
|
||
|
||
void AEBGun::MulticastShotFired_Implementation()
|
||
{
|
||
// Handle client-side shot fired effects
|
||
// This could include muzzle flash, sound effects, etc.
|
||
}
|