// 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 a scene component as root to hold both mesh components USceneComponent* RootScene = CreateDefaultSubobject(TEXT("RootScene")); RootComponent = RootScene; // Create both mesh components and attach to root SkeletalGunMesh = CreateDefaultSubobject(TEXT("SkeletalGunMesh")); StaticGunMesh = CreateDefaultSubobject(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(TEXT("Barrel")); Barrel->SetupAttachment(SkeletalGunMesh, TEXT("MuzzleSocket")); Barrel->SetParentGun(this); // Create the magazine component Magazine = CreateDefaultSubobject(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 (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) { 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& 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 (!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 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::ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig) { if (!NewConfig) { return; } WeaponConfig = NewConfig; // Apply gun configuration from barrel config BarrelLength = NewConfig->BarrelConfig.BarrelLength; RiflingTwist = NewConfig->BarrelConfig.RiflingTwist; BoreRadius = NewConfig->BarrelConfig.BoreRadius; MuzzleVelocityMin = NewConfig->BarrelConfig.MuzzleVelocityMin; MuzzleVelocityMax = NewConfig->BarrelConfig.MuzzleVelocityMax; // Apply fire control configuration to gun FireMode = NewConfig->FireControlConfig.DefaultFireMode; FireRateMin = NewConfig->FireControlConfig.FireRateMin / 60.0f; // Convert RPM to RPS FireRateMax = NewConfig->FireControlConfig.FireRateMax / 60.0f; BurstCount = NewConfig->FireControlConfig.BurstCount; BurstCooldown = NewConfig->FireControlConfig.BurstCooldown; GatlingSpoolUpTime = NewConfig->FireControlConfig.GatlingSpoolUpTime; GatlingSpoolDownTime = NewConfig->FireControlConfig.GatlingSpoolDownTime; bGatlingAutoSpool = NewConfig->FireControlConfig.bGatlingAutoSpool; // Apply configuration to barrel for backward compatibility if (Barrel) { // Set parent gun reference Barrel->SetParentGun(this); // Apply barrel-specific configuration Barrel->ApplyBarrelConfiguration(NewConfig->BarrelConfig); // Apply networking configuration from gun to barrel Barrel->ReplicateShotFiredEvents = NewConfig->bReplicateShotEvents; UE_LOG(LogTemp, Log, TEXT("Barrel '%s' configured by Gun '%s'"), *Barrel->GetName(), *NewConfig->GetWeaponDisplayName()); } UE_LOG(LogTemp, Log, TEXT("Gun '%s' applied configuration"), *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; } 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(SkeletalGunMesh) : Cast(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 (WeaponConfig && WeaponConfig->IsFireModeAvailable(NewFireMode)) { FireMode = NewFireMode; // Update barrel for backward compatibility // Barrel no longer keeps FireMode state. } } 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& Trajectory, TSubclassOf BulletClass, const TArray& 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 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. }