From 989c3afe3af59be689bafe0a2e421ece93f96f21 Mon Sep 17 00:00:00 2001 From: Zack3D Date: Thu, 10 Jul 2025 01:02:24 -0700 Subject: [PATCH] refact --- .claude/settings.local.json | 10 - .gitignore | 26 + Documentation/Gun_Centric_Architecture.md | 25 + Source/EasyBallistics/EasyBallistics.Build.cs | 5 +- .../EasyBallistics/Private/BarrelEvents.cpp | 105 -- Source/EasyBallistics/Private/EBBarrel.cpp | 322 ++-- Source/EasyBallistics/Private/EBBullet.cpp | 199 ++- Source/EasyBallistics/Private/EBGun.cpp | 507 ++++-- .../Private/EBPlayerWeaponController.cpp | 1365 +++++++++++++++++ .../EasyBallistics/Private/EBWeaponData.cpp | 187 +++ Source/EasyBallistics/Private/Reload.cpp | 74 +- Source/EasyBallistics/Private/Trace.cpp | 22 +- Source/EasyBallistics/Public/EBBarrel.h | 50 +- Source/EasyBallistics/Public/EBBullet.h | 34 +- .../Public/EBBulletProperties.h | 16 + Source/EasyBallistics/Public/EBGun.h | 343 ++++- .../Public/EBPlayerWeaponController.h | 429 ++++++ Source/EasyBallistics/Public/EBWeaponData.h | 619 ++++++++ .../EasyBallisticsEditor.Build.cs | 7 +- .../Private/EBBarrelComponentFactory.cpp | 25 +- .../EBPlayerWeaponControllerCustomization.cpp | 36 + .../Private/EBWeaponDataFactory.cpp | 248 +++ .../Private/EasyBallisticsEditor.cpp | 25 +- .../EBPlayerWeaponControllerCustomization.h | 19 + .../Public/EBWeaponDataFactory.h | 34 + docs/docs/advanced/researcher-guide.md | 424 +++++ .../api/parameter-validation-reference.md | 324 ++++ docs/docs/tutorials/gun-blueprint-guide.md | 316 ++-- docs/docs/tutorials/weapon-data-setup.md | 425 +++++ docs/sidebars.js | 3 + 30 files changed, 5464 insertions(+), 760 deletions(-) delete mode 100644 .claude/settings.local.json create mode 100644 Source/EasyBallistics/Private/EBPlayerWeaponController.cpp create mode 100644 Source/EasyBallistics/Private/EBWeaponData.cpp create mode 100644 Source/EasyBallistics/Public/EBPlayerWeaponController.h create mode 100644 Source/EasyBallistics/Public/EBWeaponData.h create mode 100644 Source/EasyBallisticsEditor/Private/EBPlayerWeaponControllerCustomization.cpp create mode 100644 Source/EasyBallisticsEditor/Private/EBWeaponDataFactory.cpp create mode 100644 Source/EasyBallisticsEditor/Public/EBPlayerWeaponControllerCustomization.h create mode 100644 Source/EasyBallisticsEditor/Public/EBWeaponDataFactory.h create mode 100644 docs/docs/advanced/researcher-guide.md create mode 100644 docs/docs/api/parameter-validation-reference.md create mode 100644 docs/docs/tutorials/weapon-data-setup.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index e6d7a63..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(find:*)", - "Bash(grep:*)", - "Bash(rg:*)" - ], - "deny": [] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d720aa6..50708ea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,29 @@ Binaries/ Intermediate/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +dev-debug.log +# Dependency directories +node_modules/ +# Environment variables +.env +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# OS specific +.DS_Store + +# Task files +tasks.json +tasks/ diff --git a/Documentation/Gun_Centric_Architecture.md b/Documentation/Gun_Centric_Architecture.md index 91760d7..bb3ace5 100644 --- a/Documentation/Gun_Centric_Architecture.md +++ b/Documentation/Gun_Centric_Architecture.md @@ -32,6 +32,31 @@ MyGun->PullTrigger(); - Automatic configuration from weapon assets - Full multiplayer support - Event-driven architecture +- Support for both skeletal and static mesh guns +- Flexible muzzle positioning (socket-based for skeletal, transform-based for static) + +### Mesh Configuration + +The gun actor supports both skeletal and static mesh setups: + +**Skeletal Mesh Mode (Default):** +```cpp +// Set gun to use skeletal mesh (default) +MyGun->SetMeshType(true); +MyGun->SetMuzzleSocketName("MuzzleFlash"); // Use socket for muzzle position +``` + +**Static Mesh Mode:** +```cpp +// Set gun to use static mesh +MyGun->SetMeshType(false); +MyGun->MuzzleTransform = FTransform(FVector(50, 0, 0)); // Set relative muzzle position +``` + +**Key Differences:** +- Skeletal meshes use sockets for precise muzzle positioning and can have animations +- Static meshes use relative transforms and are more performance-friendly for simple weapons +- Both modes maintain full compatibility with the firing system ### Magazine Component (UEBMagazine) diff --git a/Source/EasyBallistics/EasyBallistics.Build.cs b/Source/EasyBallistics/EasyBallistics.Build.cs index a29a333..b728317 100644 --- a/Source/EasyBallistics/EasyBallistics.Build.cs +++ b/Source/EasyBallistics/EasyBallistics.Build.cs @@ -12,7 +12,10 @@ public class EasyBallistics : ModuleRules PublicDependencyModuleNames.AddRange( new string[] { - "Core" + "Core", + "Niagara", + "NiagaraCore", + "NiagaraShader" // ... add other public dependencies that you statically link with here ... } ); diff --git a/Source/EasyBallistics/Private/BarrelEvents.cpp b/Source/EasyBallistics/Private/BarrelEvents.cpp index 4f7af6f..dd3f450 100644 --- a/Source/EasyBallistics/Private/BarrelEvents.cpp +++ b/Source/EasyBallistics/Private/BarrelEvents.cpp @@ -3,111 +3,6 @@ #include "EBBarrel.h" #include "Net/UnrealNetwork.h" -#define REPOWNERONLY false - void UEBBarrel::ShotFiredMulticast_Implementation() { ShotFired.Broadcast(); -} - - -void UEBBarrel::Shoot(bool Trigger) { - if (ClientSideAim && GetOwner()->GetRemoteRole() == ROLE_Authority && Trigger) { - Aim = GetComponentTransform().GetUnitAxis(EAxis::X); - Location = GetComponentTransform().GetLocation(); - ShootRepCSA(Trigger, UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(), Location), Aim); - } - else { - ShootRep(Trigger); - } -} - -void UEBBarrel::ShootRep_Implementation(bool Trigger) { - if (Trigger) { - if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) { - BurstRemaining = BurstCount; - }; - Shooting = true; - } - else { - //burst cannot be interrupted - if (FireMode != EFireMode::FM_Burst || BurstRemaining<=0) { - Shooting = false; - } - } -} - -bool UEBBarrel::ShootRep_Validate(bool Trigger) { - return true; -} - -void UEBBarrel::ShootRepCSA_Implementation(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { - Location = UGameplayStatics::RebaseZeroOriginOntoLocal(GetWorld(), NewLocation); - Aim = NewAim; - RemoteAimReceived = true; - - if (Trigger) { - if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) { - BurstRemaining = BurstCount; - }; - Shooting = true; - } - else { - //burst cannot be interrupted - if (FireMode != EFireMode::FM_Burst || BurstRemaining <= 0) { - Shooting = false; - } - } -} - -bool UEBBarrel::ShootRepCSA_Validate(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { - return true; -} - -void UEBBarrel::GatlingSpool_Implementation(bool Spool) { - Spooling = Spool; -} - -bool UEBBarrel::GatlingSpool_Validate(bool Spool) { - return true; -} - -void UEBBarrel::SwitchFireMode_Implementation(EFireMode NewFireMode) { - FireMode = NewFireMode; -} -bool UEBBarrel::SwitchFireMode_Validate(EFireMode NewFireMode) { - return true; -} - -void UEBBarrel::ClientAim_Implementation(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { - Location = UGameplayStatics::RebaseZeroOriginOntoLocal(GetWorld(),NewLocation); - Aim = NewAim; - RemoteAimReceived = true; -} -bool UEBBarrel::ClientAim_Validate(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { - return true; -} - -void UEBBarrel::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const -{ - Super::GetLifetimeReplicatedProps(OutLifetimeProps); - -#if REPOWNERONLY - DOREPLIFETIME_CONDITION(UEBBarrel, FireMode, COND_OwnerOnly); - DOREPLIFETIME_CONDITION(UEBBarrel, CycleAmmoCount, COND_OwnerOnly); - DOREPLIFETIME_CONDITION(UEBBarrel, CycleAmmoPos, COND_OwnerOnly); - DOREPLIFETIME_CONDITION(UEBBarrel, Ammo, COND_OwnerOnly); - DOREPLIFETIME_CONDITION(UEBBarrel, ChamberedBullet, COND_OwnerOnly); - DOREPLIFETIME_CONDITION(UEBBarrel, Shooting, COND_OwnerOnly); - DOREPLIFETIME_CONDITION(UEBBarrel, ShootingBlocked, COND_OwnerOnly); - DOREPLIFETIME_CONDITION(UEBBarrel, Spooling, COND_OwnerOnly); -#else - DOREPLIFETIME(UEBBarrel, FireMode); - DOREPLIFETIME(UEBBarrel, CycleAmmoCount); - DOREPLIFETIME(UEBBarrel, CycleAmmoPos); - DOREPLIFETIME(UEBBarrel, Ammo); - DOREPLIFETIME(UEBBarrel, ChamberedBullet); - DOREPLIFETIME(UEBBarrel, Shooting); - DOREPLIFETIME(UEBBarrel, ShootingBlocked); - DOREPLIFETIME(UEBBarrel, Spooling); -#endif } \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBBarrel.cpp b/Source/EasyBallistics/Private/EBBarrel.cpp index 1863318..0da3bbd 100644 --- a/Source/EasyBallistics/Private/EBBarrel.cpp +++ b/Source/EasyBallistics/Private/EBBarrel.cpp @@ -14,227 +14,40 @@ UEBBarrel::UEBBarrel() { SetIsReplicatedByDefault(ReplicateVariables); RandomStream.GenerateNewSeed(); - - GatlingRPS = FireRateMin; } void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); - if (ClientSideAim){ - if (GetOwner()->GetRemoteRole()==ROLE_Authority){ - TimeSinceAimUpdate += DeltaTime; - - if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) { - Aim = GetComponentTransform().GetUnitAxis(EAxis::X); - Location = GetComponentTransform().GetLocation(); - ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim); - TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency); - }; - }else{ - if (!RemoteAimReceived) { - Aim = GetComponentTransform().GetUnitAxis(EAxis::X); - Location = GetComponentTransform().GetLocation(); - } - else { - FVector LocOffset = (Location - GetComponentLocation()); - if (LocOffset.Size() > ClientAimDistanceLimit) { - //lag or cheater??? - Location = GetComponentLocation() + LocOffset.GetSafeNormal()*ClientAimDistanceLimit; - } - } - } - } - else { - Aim = GetComponentTransform().GetUnitAxis(EAxis::X); - Location = GetComponentTransform().GetLocation(); - } - - //Only server can tick - if (GetOwner()->GetLocalRole() == ROLE_Authority){ - - float RemainingDelta; - - if (FireMode == EFireMode::FM_Gatling) { - if (Spooling || (GatlingAutoSpool && Shooting)) { - GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMax, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f)); - } - else { - GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMin, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f)); - } - GatlingPhase += GatlingRPS*DeltaTime; - for (int i = 1; i <= GatlingPhase; i++) { - if (Cooldown <= 0.0f && LoadNext) { - NextBullet(); - } - - if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) { - SpawnBullet(GetOwner(), Location, Aim); - } - } - GatlingPhase = FMath::Fmod(GatlingPhase, 1.0f); - - } - else { - RemainingDelta = DeltaTime; - do { - float step = FMath::Min(Cooldown, RemainingDelta); - - Cooldown -= step; - - RemainingDelta -= step; - - if (Cooldown <= 0.0f && LoadNext) { - NextBullet(); - } - - //shoot when ready - if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) { - if (BurstRemaining > 0 || (FireMode != EFireMode::FM_Burst && FireMode != EFireMode::FM_InterBurst)) { - SpawnBullet(GetOwner(), Location, Aim); - } - else { - Shooting = false; - } - } - } while (RemainingDelta > 0 && Cooldown > 0); - } - } + // The barrel's tick function is now primarily for debug displays or other non-firing logic. + // All firing state, cooldowns, and ammo management has been moved to AEBGun. } -void UEBBarrel::NextBullet() { - if (ChamberedBullet == nullptr) { - if (Ammo.Num() > 0 && (CycleAmmoCount > 0 || CycleAmmoUnlimited || (!CycleAmmo))) { - - //cycle ammo - if (CycleAmmo) { - if (CycleAmmoPos >= Ammo.Num()) { CycleAmmoPos = 0; } - ChamberedBullet = Ammo[CycleAmmoPos]; - CycleAmmoPos++; - - if (!CycleAmmoUnlimited) { - CycleAmmoCount--; - } - } - else { - ChamberedBullet = Ammo[0]; - Ammo.RemoveAt(0, 1, EAllowShrinking::Yes); - } - - ReadyToShoot.Broadcast(); - } - else { - AmmoDepleted.Broadcast(); - } - } +bool UEBBarrel::ClientAim_Validate(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { + return true; +} +void UEBBarrel::ClientAim_Implementation(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { + Location = NewLocation; + Aim = NewAim; + TimeSinceAimUpdate = 0.0f; + RemoteAimReceived = true; } -void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim) { - TSubclassOf BulletClass = ChamberedBullet; +bool UEBBarrel::ShootRep_Validate(bool Trigger) { + return true; +} +void UEBBarrel::ShootRep_Implementation(bool Trigger) { + // Legacy function, logic moved to AEBGun. + // This can be left empty or used for simple replication effects. +} - if (BulletClass != nullptr) { - FVector OutLocation; - FVector OutAim; - - InitialBulletTransform(InLocation, InAim, OutLocation, OutAim); - - AEBBullet* Default = Cast(BulletClass->GetDefaultObject()); - - float BulletSpread = Default->Spread; - if (Default->SpreadBias > 0.0f) { - float SpreadMult = FMath::Pow(FMath::FRand(), Default->SpreadBias); - BulletSpread *= SpreadMult; - } - float BarrelSpread = Spread; - if (SpreadBias > 0.0f) { - float SpreadMult = FMath::Pow(FMath::FRand(), SpreadBias); - BarrelSpread *= SpreadMult; - } - - float TotalSpread = BulletSpread+BarrelSpread; - - OutAim = RandomStream.VRandCone(OutAim,TotalSpread); - float BulletVelocity = FMath::Lerp(MuzzleVelocityMultiplierMin* Default->MuzzleVelocityMin, MuzzleVelocityMultiplierMax*Default->MuzzleVelocityMax, RandomStream.FRand()); - FVector Velocity = OutAim*BulletVelocity; - - //get parent physics body - UPrimitiveComponent* parent = Cast(GetAttachParent()); - Velocity += AdditionalVelocity; - - if (parent != nullptr) { - - if (parent->IsSimulatingPhysics()) { - Velocity += parent->GetPhysicsLinearVelocityAtPoint(OutLocation)*InheritVelocity; - } - - if (Default->Shotgun) { - ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier*Default->ShotCount); - } - else{ - ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier); - } - } - - BeforeShotFired.Broadcast(); - - AEBBullet::SpawnWithExactVelocityFromBarrel(BulletClass, Owner, Owner->GetInstigator(), OutLocation, Velocity, this); - - //spend ammo - ChamberedBullet = nullptr; - if (FireMode != EFireMode::FM_Gatling) { - Cooldown = 1.0f / FMath::Lerp(FireRateMin, FireRateMax, RandomStream.FRand()); - } - - //fire modes - switch (FireMode) { - case EFireMode::FM_Auto: - LoadNext = true; - break; - - case EFireMode::FM_Burst: - LoadNext = true; - break; - - case EFireMode::FM_InterBurst: - LoadNext = true; - break; - - case EFireMode::FM_Semiauto: - Shooting = false; - LoadNext = true; - break; - - case EFireMode::FM_Manual: - Shooting = false; - LoadNext = false; - break; - - case EFireMode::FM_Slamfire: - LoadNext = false; - break; - - case EFireMode::FM_Gatling: - LoadNext = true; - break; - }; - - if (BurstRemaining > 0) { - BurstRemaining--; - } - else { - if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) { - Cooldown = FMath::Max(Cooldown, BurstCooldown); - } - } - - if (ReplicateShotFiredEvents) { - ShotFiredMulticast(); - } - else { - ShotFired.Broadcast(); - } - } +bool UEBBarrel::ShootRepCSA_Validate(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { + return true; +} +void UEBBarrel::ShootRepCSA_Implementation(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) { + ClientAim_Implementation(NewLocation, NewAim); + ShootRep_Implementation(Trigger); } void UEBBarrel::InitialBulletTransform_Implementation(FVector InLocation, FVector InDirection, FVector& OutLocation, FVector& OutDirection) { @@ -477,4 +290,93 @@ bool UEBBarrel::CanFireFromGun() const return ParentGun->CanFire(); } return false; +} + +// =============================== +// Refactored Single-Shot Helper +// =============================== + +void UEBBarrel::FireBullet(TSubclassOf BulletClass) +{ + // Validate input + if (!BulletClass) + { + UE_LOG(LogTemp, Warning, TEXT("UEBBarrel::FireBullet called with invalid BulletClass")); + return; + } + + AActor* OwnerActor = GetOwner(); + if (!OwnerActor) + { + UE_LOG(LogTemp, Warning, TEXT("UEBBarrel::FireBullet called but barrel has no owner")); + return; + } + + // Determine current muzzle location & aim direction + FVector InLocation = GetComponentTransform().GetLocation(); + FVector InDirection = GetComponentTransform().GetUnitAxis(EAxis::X); + + FVector OutLocation; + FVector OutDirection; + InitialBulletTransform(InLocation, InDirection, OutLocation, OutDirection); + + // Calculate spread and velocity using same logic as original SpawnBullet + AEBBullet* Default = Cast(BulletClass->GetDefaultObject()); + if (!Default) + { + UE_LOG(LogTemp, Warning, TEXT("UEBBarrel::FireBullet could not get default object for bullet class")); + return; + } + + float BulletSpread = Default->Spread; + if (Default->SpreadBias > 0.0f) + { + float SpreadMult = FMath::Pow(FMath::FRand(), Default->SpreadBias); + BulletSpread *= SpreadMult; + } + float BarrelSpread = Spread; + if (SpreadBias > 0.0f) + { + float SpreadMult = FMath::Pow(FMath::FRand(), SpreadBias); + BarrelSpread *= SpreadMult; + } + + float TotalSpread = BulletSpread + BarrelSpread; + OutDirection = RandomStream.VRandCone(OutDirection, TotalSpread); + + float BulletVelocity = FMath::Lerp(MuzzleVelocityMultiplierMin * Default->MuzzleVelocityMin, MuzzleVelocityMultiplierMax * Default->MuzzleVelocityMax, RandomStream.FRand()); + FVector Velocity = OutDirection * BulletVelocity; + + // Inherit velocity from parent physics body if applicable + UPrimitiveComponent* ParentComp = Cast(GetAttachParent()); + Velocity += AdditionalVelocity; + if (ParentComp && ParentComp->IsSimulatingPhysics()) + { + Velocity += ParentComp->GetPhysicsLinearVelocityAtPoint(OutLocation) * InheritVelocity; + } + + // Apply recoil impulse + if (ParentComp && ParentComp->IsSimulatingPhysics()) + { + FVector Impulse = -Velocity * Default->Mass * RecoilMultiplier * (Default->Shotgun ? Default->ShotCount : 1); + ApplyRecoil(ParentComp, OutLocation, Impulse); + } + + // Broadcast pre-shot event + BeforeShotFired.Broadcast(); + + // Actually spawn the bullet with exact velocity + AEBBullet::SpawnWithExactVelocityFromBarrel(BulletClass, OwnerActor, OwnerActor->GetInstigator(), OutLocation, Velocity, this); + + // Cooldown logic is now managed by the gun/controller – barrel just fires once. + + // Notify listeners that shot has been fired + if (ReplicateShotFiredEvents) + { + ShotFiredMulticast(); + } + else + { + ShotFired.Broadcast(); + } } \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBBullet.cpp b/Source/EasyBallistics/Private/EBBullet.cpp index 1e672ae..530a0b9 100644 --- a/Source/EasyBallistics/Private/EBBullet.cpp +++ b/Source/EasyBallistics/Private/EBBullet.cpp @@ -2,11 +2,20 @@ #include "EBBullet.h" #include "EBBarrel.h" #include "EBUnitConversions.h" +#include "Kismet/KismetMathLibrary.h" // Sets default values AEBBullet::AEBBullet() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; + SetActorTickEnabled(true); + + bHasSpalled = false; + + Collision = CreateDefaultSubobject(TEXT("Collision")); + RootComponent = Collision; + Collision->SetCollisionProfileName(TEXT("BlockAllDynamic")); SetTickGroup(ETickingGroup::TG_PrePhysics); // Set bullet lifetime to 10 seconds @@ -256,6 +265,35 @@ float AEBBullet::GetCurveValue(const UCurveFloat* curve, float in, float deflt) return curve->GetFloatValue(in); } +// Bullet-type specific scaling for spalling behaviour +float AEBBullet::GetSpallFactorByBulletType() const +{ + if (!BulletPropertiesAsset) + { + return 1.0f; + } + + switch (BulletPropertiesAsset->BulletProperties.BulletType) + { + case EBulletType::BT_ArmorPiercing: + case EBulletType::BT_ArmorPiercingIncendiary: + return 1.4f; // More prone to cause spall + case EBulletType::BT_FullMetalJacket: + case EBulletType::BT_Match: + case EBulletType::BT_Tracer: + return 1.0f; // Baseline + case EBulletType::BT_HollowPoint: + case EBulletType::BT_SoftPoint: + case EBulletType::BT_Frangible: + return 0.6f; // Less spall (expands/deforms instead) + case EBulletType::BT_Wadcutter: + case EBulletType::BT_SemiWadcutter: + return 0.8f; // Moderate + default: + return 1.0f; + } +} + void AEBBullet::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) { Super::ApplyWorldOffset(InOffset, bWorldShift); LastTraceStart += InOffset; @@ -359,6 +397,11 @@ FMathematicalMaterialProperties AEBBullet::GetMaterialProperties(UPhysicalMateri // Spalling Implementation bool AEBBullet::ShouldGenerateSpalling(FVector ImpactVelocity, UPhysicalMaterial* Material) const { + if (bHasSpalled) + { + return false; + } + if (!EnableSpalling || IsSpallFragment || !Material || !MaterialResponseMap) { return false; @@ -371,25 +414,28 @@ bool AEBBullet::ShouldGenerateSpalling(FVector ImpactVelocity, UPhysicalMaterial } float ImpactSpeed = ImpactVelocity.Size(); - + + // Apply bullet-type scaling (AP increases spall likelihood, HP decreases) + float BulletTypeFactor = GetSpallFactorByBulletType(); + // Calculate dynamic spalling threshold based on material properties float MaterialSpallResistance = Material->Density * 0.1f; // Denser materials resist spalling more float EffectiveThreshold = ResponseEntry->SpallVelocityThreshold * (1.0f + MaterialSpallResistance); - + // Reduce/increase threshold according to bullet type + EffectiveThreshold /= BulletTypeFactor; + // Check if bullet has enough kinetic energy to cause spalling float ImpactEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed; float MinimumSpallEnergy = 50.0f * Material->Density; // Energy threshold scales with material density - + MinimumSpallEnergy /= BulletTypeFactor; + return ImpactSpeed >= EffectiveThreshold && ImpactEnergy >= MinimumSpallEnergy; } -void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, UPhysicalMaterial* Material, AActor* HitActor) +void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, + float PlateThicknessCM, float ImpactAngleDegrees, + UPhysicalMaterial* Material, AActor* HitActor) { - if (!ShouldGenerateSpalling(ImpactVelocity, Material)) - { - return; - } - // SANITY CHECK: Validate input parameters if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z) || !FMath::IsFinite(ImpactVelocity.X) || !FMath::IsFinite(ImpactVelocity.Y) || !FMath::IsFinite(ImpactVelocity.Z) || @@ -399,6 +445,31 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel return; } + // Calculate cone half-angle (deg) based on empirical model θ = 25° + 10°·sin(impactAngle) + float ConeHalfAngleDeg = 25.0f + 10.0f * FMath::Sin(FMath::DegreesToRadians(ImpactAngleDegrees)); + ConeHalfAngleDeg = FMath::Clamp(ConeHalfAngleDeg, 20.0f, 45.0f); + float ConeHalfAngleRad = FMath::DegreesToRadians(ConeHalfAngleDeg); + + // Determine total spall mass from target plate (NOT bullet) if we know thickness + float TotalSpallMassKg = 0.0f; + if (PlateThicknessCM > 0.01f && Material) + { + float t_m = PlateThicknessCM * 0.01f; // cm → m + float r_m = t_m * FMath::Tan(ConeHalfAngleRad); + float Volume_m3 = (1.0f/3.0f) * PI * r_m * r_m * t_m; // cone volume + // Density – assume UPhysicalMaterial::Density (kg/m³). If unreal default is g/cm³ convert, but here treat as kg/m³. + float TargetDensity = Material->Density > 1.0f ? Material->Density : Material->Density * 1000.0f; // fallback + TotalSpallMassKg = Volume_m3 * TargetDensity; + // Clamp to at most the bullet mass *2 to keep extremes sane + TotalSpallMassKg = FMath::Clamp(TotalSpallMassKg, 0.001f, GetEffectiveMass() * 2.0f); + } + + if (TotalSpallMassKg <= 0.0f) + { + // Fallback: use percentage of bullet mass (old behaviour ~20%) + TotalSpallMassKg = GetEffectiveMass() * 0.2f; + } + // SANITY CHECK: Prevent excessive fragment generation for performance static int32 GlobalFragmentCount = 0; static float LastFrameTime = 0.0f; @@ -430,10 +501,6 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel FragmentClass = GetClass(); } - float ImpactSpeed = ImpactVelocity.Size(); - float ImpactAngle = FMath::Acos(FMath::Abs(FVector::DotProduct(ImpactVelocity.GetSafeNormal(), ImpactNormal))); - float ImpactAngleDegrees = FMath::RadiansToDegrees(ImpactAngle); - int32 FragmentCount; float SpreadAngleRad; @@ -445,7 +512,7 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel if (UseMathematicalSpalling) { // Use mathematical ballistics for spalling calculations - float VelocityMPS = ImpactSpeed / 100.0f; // Convert cm/s to m/s + float VelocityMPS = ImpactVelocity.Size() / 100.0f; // Convert cm/s to m/s FragmentCount = UEBMathematicalBallistics::CalculateSpallFragmentCount( BulletPropertiesAsset->BulletProperties, @@ -466,26 +533,63 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel else { // Use artistic spalling calculations (original physics-based approach) - float ImpactKineticEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed; - float AngleEffectiveness = FMath::Cos(ImpactAngle); + float ImpactKineticEnergy = 0.5f * GetEffectiveMass() * ImpactVelocity.Size() * ImpactVelocity.Size(); + float AngleEffectiveness = FMath::Cos(FMath::DegreesToRadians(ImpactAngleDegrees)); float MaterialHardness = Material ? FMath::Clamp(Material->Density * 0.001f, 0.1f, 5.0f) : 1.0f; float BaseFragmentCount = FMath::Sqrt(ImpactKineticEnergy / 1000.0f) * MaterialHardness * AngleEffectiveness; FragmentCount = FMath::Clamp(FMath::RoundToInt(BaseFragmentCount), 1, ResponseEntry->SpallFragmentCount); - float PhysicalSpreadAngle = FMath::Lerp(15.0f, 60.0f, FMath::Sin(ImpactAngle)); + float PhysicalSpreadAngle = FMath::Lerp(15.0f, 60.0f, FMath::Sin(FMath::DegreesToRadians(ImpactAngleDegrees))); SpreadAngleRad = FMath::DegreesToRadians(PhysicalSpreadAngle); } // SANITY CHECK: Clamp fragment count to reasonable limits const int32 MaxFragmentsPerImpact = 20; // Absolute maximum per single impact - FragmentCount = FMath::Clamp(FragmentCount, 0, MaxFragmentsPerImpact); - + float BulletTypeFactor = GetSpallFactorByBulletType(); + FragmentCount = FMath::Clamp(FMath::RoundToInt(FragmentCount * BulletTypeFactor), 0, MaxFragmentsPerImpact); + if (FragmentCount <= 0) { return; } + // Distribute parent bullet's mass among fragments to ensure conservation + TArray MassRatios; + float TotalMassRatioPart = 0.f; + + for (int32 i = 0; i < FragmentCount; ++i) + { + float ratio; + if (UseMathematicalSpalling) + { + ratio = UEBMathematicalBallistics::CalculateSpallFragmentMass(BulletPropertiesAsset->BulletProperties, ResponseEntry->MathematicalProperties, i, FragmentCount); + } + else + { + // Using a simple power law for mass distribution. Smaller fragments are more common. + ratio = FMath::Pow(RandomStream.FRand(), 2.0f) + 0.1f; + } + MassRatios.Add(ratio); + TotalMassRatioPart += ratio; + } + + // Normalize the mass ratios so they sum to 1.0 + if (TotalMassRatioPart > 0.f) + { + for (int32 i = 0; i < FragmentCount; ++i) + { + MassRatios[i] /= TotalMassRatioPart; + } + } + else // Should not happen if FragmentCount > 0, but as a fallback + { + for (int32 i = 0; i < FragmentCount; ++i) + { + MassRatios.Add(1.0f / FragmentCount); + } + } + // Base spalling direction (combination of reflection and surface normal) FVector ReflectionDirection = UKismetMathLibrary::GetReflectionVector(ImpactVelocity.GetSafeNormal(), ImpactNormal); FVector SpallBaseDirection = FMath::Lerp(ImpactNormal, ReflectionDirection, 0.3f).GetSafeNormal(); @@ -502,44 +606,31 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel { float FragmentMass; float FragmentSpeed; + float FragmentMassRatio = MassRatios[i]; + // Determine fragment mass using Mott distribution (simple inverse CDF) + float xi = RandomStream.FRand(); + float m_char = TotalSpallMassKg / FragmentCount; // characteristic mass, rough + float fragMassMott = FMath::Square(-FMath::Loge(FMath::Clamp(1.0f - xi, 0.0001f, 0.9999f))) * m_char; + FragmentMass = FMath::Clamp(fragMassMott, 0.0001f, TotalSpallMassKg * 0.5f); + + // Compute velocity using a Gurney-like energy share + float VelocityMPS; if (UseMathematicalSpalling) { - // Mathematical fragment calculations - float VelocityMPS = ImpactSpeed / 100.0f; // Convert cm/s to m/s - - float FragmentMassRatio = UEBMathematicalBallistics::CalculateSpallFragmentMass( + float VelocityMPS_impact = ImpactVelocity.Size() / 100.0f; + VelocityMPS = UEBMathematicalBallistics::CalculateSpallFragmentVelocity( BulletPropertiesAsset->BulletProperties, ResponseEntry->MathematicalProperties, - i, FragmentCount - ); - - FragmentMass = GetEffectiveMass() * FragmentMassRatio; - - float FragmentVelocityMPS = UEBMathematicalBallistics::CalculateSpallFragmentVelocity( - BulletPropertiesAsset->BulletProperties, - ResponseEntry->MathematicalProperties, - VelocityMPS, - FragmentMassRatio - ); - - FragmentSpeed = FragmentVelocityMPS * 100.0f; // Convert back to cm/s + VelocityMPS_impact, + FragmentMass / TotalSpallMassKg); } else { - // Artistic fragment calculations - float ImpactKineticEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed; - float AngleEffectiveness = FMath::Cos(ImpactAngle); - float SpallEnergyFraction = FMath::Clamp(0.05f + (AngleEffectiveness * 0.15f), 0.05f, 0.2f); - float AvailableSpallEnergy = ImpactKineticEnergy * SpallEnergyFraction; - float EnergyPerFragment = AvailableSpallEnergy / FragmentCount; - - float MassFraction = RandomStream.FRandRange(0.02f, 0.2f) * ResponseEntry->SpallMassMultiplier; - FragmentMass = GetEffectiveMass() * MassFraction; - - float FragmentKineticEnergy = EnergyPerFragment * RandomStream.FRandRange(0.5f, 1.5f); - FragmentSpeed = FMath::Sqrt(2.0f * FragmentKineticEnergy / FragmentMass); + float AvailableEnergy = 0.5f * TotalSpallMassKg * FMath::Square(ImpactVelocity.Size() / 100.0f) * 0.15f; // 15% of bullet KE + VelocityMPS = FMath::Sqrt(2.0f * (AvailableEnergy / FragmentCount) / FragmentMass); } + FragmentSpeed = VelocityMPS * 100.0f; // m/s → cm/s // SANITY CHECK: Validate fragment properties if (FragmentMass <= 0.0f || !FMath::IsFinite(FragmentMass)) @@ -653,6 +744,18 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel OnSpallFragmentGenerated(SpawnLocation, FragmentVelocity, FragmentMass, Material); } + + // ----- Reduce parent bullet (mass & diameter) to account for material lost ----- + float ParentMassKg = GetEffectiveMass(); + float RemainingMassKg = FMath::Max(ParentMassKg - TotalSpallMassKg, 0.001f); + if (RemainingMassKg < ParentMassKg) + { + float MassScale = RemainingMassKg / ParentMassKg; + Mass = RemainingMassKg; + // Diameter scales with cube-root of mass for similar density + float DiamScale = FMath::Pow(MassScale, 1.0f / 3.0f); + Diameter *= DiamScale; + } } void AEBBullet::OnSpallFragmentGenerated_Implementation(FVector FragmentLocation, FVector FragmentVelocity, float FragmentMass, UPhysicalMaterial* Material) diff --git a/Source/EasyBallistics/Private/EBGun.cpp b/Source/EasyBallistics/Private/EBGun.cpp index 05a5d80..86a553a 100644 --- a/Source/EasyBallistics/Private/EBGun.cpp +++ b/Source/EasyBallistics/Private/EBGun.cpp @@ -14,13 +14,24 @@ AEBGun::AEBGun() bReplicates = true; SetReplicateMovement(true); - // Create the gun mesh component - GunMesh = CreateDefaultSubobject(TEXT("GunMesh")); - RootComponent = GunMesh; + // 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 + // Create the barrel component for backward compatibility Barrel = CreateDefaultSubobject(TEXT("Barrel")); - Barrel->SetupAttachment(GunMesh, TEXT("MuzzleSocket")); + Barrel->SetupAttachment(SkeletalGunMesh, TEXT("MuzzleSocket")); Barrel->SetParentGun(this); // Create the magazine component @@ -36,11 +47,49 @@ AEBGun::AEBGun() 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) @@ -69,10 +118,53 @@ void AEBGun::Tick(float 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); @@ -82,12 +174,16 @@ void AEBGun::GetLifetimeReplicatedProps(TArray& OutLifetimePr 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 @@ -98,16 +194,18 @@ void AEBGun::PullTrigger() 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")); } } @@ -128,15 +226,11 @@ void AEBGun::ServerReleaseTrigger_Implementation() bTriggerPressed = false; // Handle fire mode specific behavior - if (Barrel && WeaponConfig) + if (FireMode == EFireMode::FM_Semiauto || FireMode == EFireMode::FM_Manual) { - EFireMode CurrentFireMode = Barrel->FireMode; - if (CurrentFireMode == EFireMode::FM_Semiauto || CurrentFireMode == EFireMode::FM_Manual) + if (CurrentGunState == EGunState::GS_Firing) { - if (CurrentGunState == EGunState::GS_Firing) - { - SetGunState(EGunState::GS_Ready); - } + SetGunState(EGunState::GS_Ready); } } } @@ -311,6 +405,7 @@ bool AEBGun::CanFire() const { if (bSafetyOn) { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() - Safety is ON")); return false; } @@ -319,11 +414,13 @@ bool AEBGun::CanFire() const 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; } @@ -375,7 +472,24 @@ void AEBGun::ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig) WeaponConfig = NewConfig; - // Apply configuration to barrel + // 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 @@ -383,27 +497,14 @@ void AEBGun::ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig) // 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; + + // Apply networking configuration from gun to barrel 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()); + 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) @@ -444,20 +545,15 @@ void AEBGun::ServerSetFireMode_Implementation(EFireMode NewFireMode) return; } - if (Barrel) - { - Barrel->FireMode = NewFireMode; - } + FireMode = NewFireMode; + + // Update barrel for backward compatibility + // Barrel no longer keeps FireMode state. } EFireMode AEBGun::GetFireMode() const { - if (Barrel) - { - return Barrel->FireMode; - } - - return EFireMode::FM_Semiauto; + return FireMode; } void AEBGun::SetGunState(EGunState NewState) @@ -489,48 +585,51 @@ 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 - if (Barrel && WeaponConfig) + switch (FireMode) { - EFireMode CurrentFireMode = Barrel->FireMode; - - switch (CurrentFireMode) + case EFireMode::FM_Semiauto: + case EFireMode::FM_Manual: + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() - FM_Semiauto or FM_Manual")); + if (!bTriggerPressed) { - 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; + 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 @@ -539,43 +638,49 @@ void AEBGun::ProcessFiring() 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) { - // 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) + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] FireShot() - Firing shot from barrel")); + + // Ensure we have a valid bullet class to spawn + if (!ChamberedBulletType->BulletClass) { - // 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 + 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 the shot - Barrel->Shoot(true); - - // Clear chambered round + + // 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 - float FireRate = WeaponConfig ? WeaponConfig->GetEffectiveFireRate(GetFireMode()) : 600.0f; - float CooldownTime = 60.0f / FireRate; // Convert RPM to seconds - + + // 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()) { @@ -650,39 +755,47 @@ void AEBGun::MulticastOnSafetyChanged_Implementation(bool bNewSafetyState) } } -// Barrel Integration Functions +// Gun Properties Functions (moved from barrel) float AEBGun::GetBarrelLength() const { - if (Barrel) - { - return Barrel->BarrelLength; - } - return 0.0f; + return BarrelLength; } float AEBGun::GetEffectiveMuzzleVelocity() const { - if (Barrel && ChamberedBulletType) + if (ChamberedBulletType) { - return Barrel->CalculateEffectiveMuzzleVelocity(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 { - if (Barrel) - { - return Barrel->CalculateBarrelAccuracy(); - } - return 0.0f; + // 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 (Barrel && ChamberedBulletType) + if (ChamberedBulletType) { - return Barrel->CalculateRecoilImpulse(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; } @@ -690,4 +803,188 @@ FVector AEBGun::CalculateRecoilImpulse() const 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. } \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBPlayerWeaponController.cpp b/Source/EasyBallistics/Private/EBPlayerWeaponController.cpp new file mode 100644 index 0000000..0e1ece9 --- /dev/null +++ b/Source/EasyBallistics/Private/EBPlayerWeaponController.cpp @@ -0,0 +1,1365 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#include "EBPlayerWeaponController.h" +#include "EBGun.h" +#include "EBWeaponData.h" +#include "EBMagazine.h" +#include "EBBullet.h" +#include "Net/UnrealNetwork.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "GameFramework/Actor.h" +#include "Components/SceneComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Components/StaticMeshComponent.h" + +UEBPlayerWeaponController::UEBPlayerWeaponController() +{ + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(true); + + // Initialize defaults + WeaponActor = nullptr; + CurrentWeaponData = nullptr; + CurrentWeaponIndex = 0; + DefaultWeapon = nullptr; + bAutoEquipOnBeginPlay = true; + + // Ammo + CurrentAmmo = 0; + ReserveAmmo = 0; + bInfiniteAmmo = false; + + // Weapon spawn settings + WeaponSpawnOffset = FTransform::Identity; + + // Niagara VFX + MuzzleFlashComponent = nullptr; + MuzzleSmokeComponent = nullptr; + BarrelHeatComponent = nullptr; + AttachmentGlowComponent = nullptr; + CurrentHeatLevel = 0.0f; + bEnableAdvancedVFX = true; +} + +void UEBPlayerWeaponController::BeginPlay() +{ + Super::BeginPlay(); + + if (HasAuthority()) + { + // Create the single persistent weapon actor + CreateWeaponActor(); + + // Create Niagara components for advanced VFX + if (bEnableAdvancedVFX) + { + CreateNiagaraComponents(); + } + + // Set up default inventory if empty + if (WeaponInventory.Num() == 0 && DefaultWeapon) + { + WeaponInventory.Add(DefaultWeapon); + } + + // Auto-equip first weapon + if (bAutoEquipOnBeginPlay && WeaponInventory.Num() > 0) + { + SwitchToWeapon(0); + } + else if (bAutoEquipOnBeginPlay && DefaultWeapon && WeaponInventory.Num() == 0) + { + // If no inventory but we have a default weapon, add it and equip it + WeaponInventory.Add(DefaultWeapon); + SwitchToWeapon(0); + } + } +} + +void UEBPlayerWeaponController::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UnbindWeaponEvents(); + + // Clean up weapon actor + if (WeaponActor) + { + WeaponActor->Destroy(); + WeaponActor = nullptr; + } + + Super::EndPlay(EndPlayReason); +} + +void UEBPlayerWeaponController::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(UEBPlayerWeaponController, WeaponActor); + DOREPLIFETIME(UEBPlayerWeaponController, CurrentWeaponData); + DOREPLIFETIME(UEBPlayerWeaponController, WeaponInventory); + DOREPLIFETIME(UEBPlayerWeaponController, CurrentWeaponIndex); + DOREPLIFETIME(UEBPlayerWeaponController, CurrentAmmo); + DOREPLIFETIME(UEBPlayerWeaponController, ReserveAmmo); + DOREPLIFETIME(UEBPlayerWeaponController, CurrentHeatLevel); +} + +#if WITH_EDITOR +void UEBPlayerWeaponController::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.Property) + { + FName PropertyName = PropertyChangedEvent.Property->GetFName(); + + // Handle weapon inventory changes + if (PropertyName == GET_MEMBER_NAME_CHECKED(UEBPlayerWeaponController, WeaponInventory) || + PropertyName == GET_MEMBER_NAME_CHECKED(UEBPlayerWeaponController, DefaultWeapon) || + PropertyName == GET_MEMBER_NAME_CHECKED(UEBPlayerWeaponController, CurrentWeaponIndex)) + { + // Create weapon actor if it doesn't exist + if (!WeaponActor) + { + CreateWeaponActor(); + } + + // Apply weapon data for immediate preview + UEBWeaponData* PreviewWeaponData = nullptr; + + // Try to get weapon from inventory first + if (WeaponInventory.IsValidIndex(CurrentWeaponIndex)) + { + PreviewWeaponData = WeaponInventory[CurrentWeaponIndex]; + } + // Fall back to default weapon + else if (DefaultWeapon) + { + PreviewWeaponData = DefaultWeapon; + } + // Or use first weapon in inventory + else if (WeaponInventory.Num() > 0) + { + PreviewWeaponData = WeaponInventory[0]; + } + + if (PreviewWeaponData) + { + CurrentWeaponData = PreviewWeaponData; + InternalApplyWeaponData(PreviewWeaponData); + } + } + } +} +#endif + +// ======================================== +// INPUT ACTIONS +// ======================================== + +void UEBPlayerWeaponController::Fire() +{ + // Debug logging if enabled + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] Fire() called - Starting fire sequence")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Weapon: %s"), CurrentWeaponData->WeaponName.ToString().IsEmpty() ? TEXT("Unnamed") : *CurrentWeaponData->WeaponName.ToString()); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Current Ammo: %d"), CurrentAmmo); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Reserve Ammo: %d"), ReserveAmmo); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Infinite Ammo: %s"), bInfiniteAmmo ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WeaponActor Valid: %s"), WeaponActor ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - CurrentWeaponData Valid: %s"), CurrentWeaponData ? TEXT("True") : TEXT("False")); + } + + if (!CanFire()) + { + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] Fire() FAILED - CanFire() returned false")); + + // Detailed failure analysis + if (!WeaponActor) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - FAILURE REASON: WeaponActor is null")); + } + else if (!CurrentWeaponData) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - FAILURE REASON: CurrentWeaponData is null")); + } + else if (CurrentAmmo <= 0) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - FAILURE REASON: No ammo (CurrentAmmo: %d)"), CurrentAmmo); + } + else if (!WeaponActor->CanFire()) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - FAILURE REASON: WeaponActor->CanFire() returned false")); + + // Additional weapon actor state info + if (WeaponActor->bSafetyOn) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WEAPON STATE: Safety is ON")); + } + + EFireMode CurrentFireMode = WeaponActor->GetFireMode(); + FString FireModeString = TEXT("Unknown"); + switch (CurrentFireMode) + { + case EFireMode::FM_Auto: FireModeString = TEXT("Auto"); break; + case EFireMode::FM_Semiauto: FireModeString = TEXT("Semi"); break; + case EFireMode::FM_Burst: FireModeString = TEXT("Burst"); break; + case EFireMode::FM_Manual: FireModeString = TEXT("Manual"); break; + case EFireMode::FM_InterBurst: FireModeString = TEXT("InterBurst"); break; + case EFireMode::FM_Slamfire: FireModeString = TEXT("Slamfire"); break; + case EFireMode::FM_Gatling: FireModeString = TEXT("Gatling"); break; + } + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WEAPON STATE: Fire Mode: %s"), *FireModeString); + } + } + return; + } + + if (!WeaponActor) + { + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] Fire() FAILED - WeaponActor is null")); + } + return; + } + + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] Fire() SUCCESS - Calling WeaponActor->PullTrigger()")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Weapon Fire Rate: %.2f RPM"), CurrentWeaponData->FireRate); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Weapon Damage: %.2f"), CurrentWeaponData->Damage); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Muzzle Velocity: %.2f cm/s"), CurrentWeaponData->MuzzleVelocity); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Heat Level: %.2f%%"), CurrentHeatLevel * 100.0f); + } + + WeaponActor->PullTrigger(); +} + +void UEBPlayerWeaponController::StopFire() +{ + if (WeaponActor) + { + WeaponActor->ReleaseTrigger(); + } +} + +void UEBPlayerWeaponController::Reload() +{ + if (WeaponActor && CurrentWeaponData) + { + // Check if we need ammo and have reserve ammo + if (CurrentAmmo < CurrentWeaponData->MagazineSize && (ReserveAmmo > 0 || bInfiniteAmmo)) + { + // Calculate ammo to reload + int32 AmmoNeeded = CurrentWeaponData->MagazineSize - CurrentAmmo; + int32 AmmoToReload = bInfiniteAmmo ? AmmoNeeded : FMath::Min(AmmoNeeded, ReserveAmmo); + + if (HasAuthority()) + { + // Update ammo + CurrentAmmo += AmmoToReload; + if (!bInfiniteAmmo) + { + ReserveAmmo -= AmmoToReload; + } + + OnAmmoChanged.Broadcast(CurrentAmmo, ReserveAmmo); + OnWeaponReloaded.Broadcast(CurrentWeaponData); + } + + // Trigger reload animation/sound on weapon + WeaponActor->ChargingHandle(); + } + } +} + +void UEBPlayerWeaponController::NextWeapon() +{ + if (WeaponInventory.Num() > 1) + { + int32 NextIndex = (CurrentWeaponIndex + 1) % WeaponInventory.Num(); + SwitchToWeapon(NextIndex); + } +} + +void UEBPlayerWeaponController::PreviousWeapon() +{ + if (WeaponInventory.Num() > 1) + { + int32 PrevIndex = (CurrentWeaponIndex - 1 + WeaponInventory.Num()) % WeaponInventory.Num(); + SwitchToWeapon(PrevIndex); + } +} + +void UEBPlayerWeaponController::SwitchToWeapon(int32 WeaponIndex) +{ + if (HasAuthority()) + { + if (WeaponInventory.IsValidIndex(WeaponIndex) && WeaponIndex != CurrentWeaponIndex) + { + CurrentWeaponIndex = WeaponIndex; + UEBWeaponData* NewWeaponData = WeaponInventory[WeaponIndex]; + + if (NewWeaponData != CurrentWeaponData) + { + ApplyWeaponData(NewWeaponData); + } + } + } + else + { + ServerSwitchToWeapon(WeaponIndex); + } +} + +void UEBPlayerWeaponController::SwitchToWeaponData(UEBWeaponData* WeaponData) +{ + if (HasAuthority()) + { + int32 WeaponIndex = FindWeaponIndex(WeaponData); + if (WeaponIndex != INDEX_NONE) + { + SwitchToWeapon(WeaponIndex); + } + } + else + { + ServerSwitchToWeaponData(WeaponData); + } +} + +void UEBPlayerWeaponController::ToggleFireMode() +{ + if (HasAuthority()) + { + if (WeaponActor && CurrentWeaponData) + { + EFireMode CurrentMode = WeaponActor->GetFireMode(); + + // Find next available fire mode + int32 CurrentModeIndex = CurrentWeaponData->AvailableFireModes.Find(CurrentMode); + if (CurrentModeIndex != INDEX_NONE) + { + int32 NextModeIndex = (CurrentModeIndex + 1) % CurrentWeaponData->AvailableFireModes.Num(); + EFireMode NextMode = CurrentWeaponData->AvailableFireModes[NextModeIndex]; + WeaponActor->SetFireMode(NextMode); + } + } + } + else + { + ServerToggleFireMode(); + } +} + +void UEBPlayerWeaponController::ToggleSafety() +{ + if (HasAuthority()) + { + if (WeaponActor && CurrentWeaponData && CurrentWeaponData->bHasSafety) + { + WeaponActor->SetSafety(!WeaponActor->bSafetyOn); + } + } + else + { + ServerToggleSafety(); + } +} + +// ======================================== +// WEAPON MANAGEMENT +// ======================================== + +void UEBPlayerWeaponController::AddWeapon(UEBWeaponData* WeaponData) +{ + if (HasAuthority()) + { + if (WeaponData && !WeaponInventory.Contains(WeaponData)) + { + WeaponInventory.Add(WeaponData); + } + } + else + { + ServerAddWeapon(WeaponData); + } +} + +void UEBPlayerWeaponController::RemoveWeapon(UEBWeaponData* WeaponData) +{ + if (HasAuthority()) + { + int32 WeaponIndex = WeaponInventory.Find(WeaponData); + if (WeaponIndex != INDEX_NONE) + { + WeaponInventory.RemoveAt(WeaponIndex); + + // Adjust current index if needed + if (CurrentWeaponIndex >= WeaponInventory.Num()) + { + CurrentWeaponIndex = FMath::Max(0, WeaponInventory.Num() - 1); + } + + // Switch to new weapon if current was removed + if (WeaponInventory.Num() > 0 && WeaponInventory.IsValidIndex(CurrentWeaponIndex)) + { + ApplyWeaponData(WeaponInventory[CurrentWeaponIndex]); + } + else + { + ApplyWeaponData(nullptr); + } + } + } + else + { + ServerRemoveWeapon(WeaponData); + } +} + +void UEBPlayerWeaponController::ClearWeapons() +{ + if (HasAuthority()) + { + WeaponInventory.Empty(); + CurrentWeaponIndex = 0; + ApplyWeaponData(nullptr); + } +} + +void UEBPlayerWeaponController::SetWeaponInventory(const TArray& NewInventory) +{ + if (HasAuthority()) + { + WeaponInventory = NewInventory; + CurrentWeaponIndex = 0; + + if (WeaponInventory.Num() > 0) + { + ApplyWeaponData(WeaponInventory[0]); + } + else + { + ApplyWeaponData(nullptr); + } + } +} + +void UEBPlayerWeaponController::ApplyWeaponData(UEBWeaponData* WeaponData) +{ + if (HasAuthority()) + { + UEBWeaponData* PreviousWeapon = CurrentWeaponData; + CurrentWeaponData = WeaponData; + + // Debug logging if enabled + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ApplyWeaponData() called")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Previous Weapon: %s"), PreviousWeapon ? *PreviousWeapon->WeaponName.ToString() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - New Weapon: %s"), WeaponData ? *WeaponData->WeaponName.ToString() : TEXT("None")); + } + + // Apply to weapon actor + InternalApplyWeaponData(WeaponData); + + // Update ammo + UpdateAmmoFromWeaponData(); + + // Broadcast event + OnWeaponChanged.Broadcast(WeaponData); + + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ApplyWeaponData() completed - weapon data applied and events broadcast")); + } + } +} + +// ======================================== +// AMMO MANAGEMENT +// ======================================== + +void UEBPlayerWeaponController::AddAmmo(int32 Amount) +{ + if (HasAuthority() && CurrentWeaponData) + { + ReserveAmmo = FMath::Min(ReserveAmmo + Amount, CurrentWeaponData->MaxReserveAmmo); + OnAmmoChanged.Broadcast(CurrentAmmo, ReserveAmmo); + } +} + +void UEBPlayerWeaponController::SetCurrentAmmo(int32 Amount) +{ + if (HasAuthority()) + { + CurrentAmmo = FMath::Max(0, Amount); + OnAmmoChanged.Broadcast(CurrentAmmo, ReserveAmmo); + } + else + { + ServerSetCurrentAmmo(Amount); + } +} + +void UEBPlayerWeaponController::SetReserveAmmo(int32 Amount) +{ + if (HasAuthority()) + { + ReserveAmmo = FMath::Max(0, Amount); + OnAmmoChanged.Broadcast(CurrentAmmo, ReserveAmmo); + } + else + { + ServerSetReserveAmmo(Amount); + } +} + +void UEBPlayerWeaponController::RefillAmmo() +{ + if (HasAuthority() && CurrentWeaponData) + { + CurrentAmmo = CurrentWeaponData->MagazineSize; + ReserveAmmo = CurrentWeaponData->MaxReserveAmmo; + OnAmmoChanged.Broadcast(CurrentAmmo, ReserveAmmo); + } +} + +// ======================================== +// STATUS QUERIES +// ======================================== + +bool UEBPlayerWeaponController::CanFire() const +{ + bool bCanFire = WeaponActor && CurrentWeaponData && CurrentAmmo > 0 && WeaponActor->CanFire(); + + // Debug logging if enabled + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() check - Result: %s"), bCanFire ? TEXT("TRUE") : TEXT("FALSE")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WeaponActor valid: %s"), WeaponActor ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - CurrentWeaponData valid: %s"), CurrentWeaponData ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - CurrentAmmo > 0: %s (%d)"), CurrentAmmo > 0 ? TEXT("True") : TEXT("False"), CurrentAmmo); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WeaponActor->CanFire(): %s"), WeaponActor ? (WeaponActor->CanFire() ? TEXT("True") : TEXT("False")) : TEXT("N/A")); + } + + return bCanFire; +} + +bool UEBPlayerWeaponController::HasWeapon() const +{ + return CurrentWeaponData != nullptr; +} + +UEBWeaponData* UEBPlayerWeaponController::GetWeaponAtIndex(int32 Index) const +{ + return WeaponInventory.IsValidIndex(Index) ? WeaponInventory[Index] : nullptr; +} + +int32 UEBPlayerWeaponController::FindWeaponIndex(UEBWeaponData* WeaponData) const +{ + return WeaponInventory.Find(WeaponData); +} + +bool UEBPlayerWeaponController::HasWeaponInInventory(UEBWeaponData* WeaponData) const +{ + return WeaponInventory.Contains(WeaponData); +} + +EFireMode UEBPlayerWeaponController::GetFireMode() const +{ + return WeaponActor ? WeaponActor->GetFireMode() : EFireMode::FM_Auto; +} + +bool UEBPlayerWeaponController::IsSafetyOn() const +{ + return WeaponActor ? WeaponActor->bSafetyOn : false; +} + +FTransform UEBPlayerWeaponController::GetMuzzleTransform() const +{ + return WeaponActor ? WeaponActor->GetMuzzleTransform() : FTransform::Identity; +} + +// ======================================== +// INTERNAL FUNCTIONS +// ======================================== + +void UEBPlayerWeaponController::CreateWeaponActor() +{ + if (!HasAuthority()) + return; + + UWorld* World = GetWorld(); + AActor* Owner = GetOwner(); + if (!World || !Owner) + return; + + // Debug logging if enabled + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CreateWeaponActor() called")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Owner: %s"), Owner ? *Owner->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - World: %s"), World ? *World->GetName() : TEXT("None")); + } + + // Calculate spawn transform + FTransform SpawnTransform = GetComponentTransform(); + SpawnTransform = SpawnTransform * WeaponSpawnOffset; + + // Spawn the single persistent weapon actor + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = Owner; + SpawnParams.Instigator = Cast(Owner); + + WeaponActor = World->SpawnActor(AEBGun::StaticClass(), SpawnTransform, SpawnParams); + + if (WeaponActor) + { + // Debug logging if enabled + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] WeaponActor spawned successfully: %s"), *WeaponActor->GetName()); + } + + // Attach weapon directly to this scene component + WeaponActor->AttachToComponent(this, FAttachmentTransformRules::SnapToTargetIncludingScale); + WeaponActor->SetActorRelativeTransform(WeaponSpawnOffset); + + // Configure collision to prevent collision with player + WeaponActor->SetActorEnableCollision(false); + + // Alternative: If you need collision enabled but want to ignore the owner + // WeaponActor->SetOwner(Owner); + // if (UPrimitiveComponent* WeaponRootComp = Cast(WeaponActor->GetRootComponent())) + // { + // WeaponRootComp->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); + // WeaponRootComp->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Ignore); + // } + + // Bind events + BindWeaponEvents(); + + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] WeaponActor setup completed - attached and events bound")); + } + } + else + { + // Debug logging for failed spawn + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Error, TEXT("[WEAPON DEBUG] FAILED to spawn WeaponActor!")); + } + } +} + + + +void UEBPlayerWeaponController::InternalApplyWeaponData(UEBWeaponData* WeaponData) +{ + if (!WeaponActor) + { + if (WeaponData && WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Error, TEXT("[WEAPON DEBUG] InternalApplyWeaponData() FAILED - WeaponActor is null")); + } + return; + } + + if (WeaponData) + { + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] InternalApplyWeaponData() called for weapon: %s"), *WeaponData->WeaponName.ToString()); + } + + // Determine which mesh type to use and apply it + bool bUsingSkeletalMesh = false; + + if (WeaponData->FirstPersonMesh) + { + // Prefer first-person skeletal mesh when available + WeaponActor->SetMeshType(true /*bUseSkeletal*/); + WeaponActor->SkeletalGunMesh->SetSkeletalMesh(WeaponData->FirstPersonMesh); + bUsingSkeletalMesh = true; + + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Using FirstPersonMesh: %s"), *WeaponData->FirstPersonMesh->GetName()); + } + } + else if (WeaponData->ThirdPersonMesh) + { + // Fallback to third-person skeletal mesh + WeaponActor->SetMeshType(true); + WeaponActor->SkeletalGunMesh->SetSkeletalMesh(WeaponData->ThirdPersonMesh); + bUsingSkeletalMesh = true; + + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Using ThirdPersonMesh: %s"), *WeaponData->ThirdPersonMesh->GetName()); + } + } + else if (WeaponData->StaticMesh) + { + // No skeletal mesh provided – use static mesh + WeaponActor->StaticGunMesh->SetStaticMesh(WeaponData->StaticMesh); + WeaponActor->SetMeshType(false /*use static*/); + bUsingSkeletalMesh = false; + + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Using StaticMesh: %s"), *WeaponData->StaticMesh->GetName()); + } + } + else + { + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - No mesh found for weapon!")); + } + } + + // Apply material overrides (works for both mesh types) + UPrimitiveComponent* TargetMeshComp = bUsingSkeletalMesh + ? Cast(WeaponActor->SkeletalGunMesh) + : Cast(WeaponActor->StaticGunMesh); + + for (int32 i = 0; i < WeaponData->MaterialOverrides.Num(); ++i) + { + if (WeaponData->MaterialOverrides[i] && TargetMeshComp) + { + TargetMeshComp->SetMaterial(i, WeaponData->MaterialOverrides[i]); + } + } + + // Apply ballistics settings + WeaponActor->MuzzleVelocityMin = WeaponData->MuzzleVelocity; + WeaponActor->MuzzleVelocityMax = WeaponData->MuzzleVelocity; + WeaponActor->FireRateMin = WeaponData->GetFireRatePerSecond(); + WeaponActor->FireRateMax = WeaponData->GetFireRatePerSecond(); + WeaponActor->BurstCount = WeaponData->BurstCount; + + // Set available fire modes + if (WeaponData->AvailableFireModes.Num() > 0) + { + WeaponActor->SetFireMode(WeaponData->DefaultFireMode); + } + + // Apply socket name or transform depending on mesh type + WeaponActor->SetMuzzleSocketName(WeaponData->MuzzleSocketName.ToString()); + + // ===== Initialize magazine with bullets and chamber first round ===== + if (HasAuthority() && WeaponActor->Magazine && WeaponData->BulletProperties) + { + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Initializing magazine with %d bullets"), WeaponData->MagazineSize); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Bullet Properties: %s"), *WeaponData->BulletProperties->GetName()); + } + + TArray Bullets; + Bullets.Reserve(WeaponData->MagazineSize); + for (int32 i = 0; i < WeaponData->MagazineSize; ++i) + { + Bullets.Add(WeaponData->BulletProperties); + } + + // Load bullets into the magazine + WeaponActor->Magazine->LoadBullets(Bullets); + + // Chamber the first round so the weapon is ready to fire + WeaponActor->ChargingHandle(); + + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Magazine loaded and first round chambered")); + } + } + else + { + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Magazine initialization skipped:")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - HasAuthority: %s"), HasAuthority() ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WeaponActor->Magazine: %s"), WeaponActor->Magazine ? TEXT("Valid") : TEXT("Null")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WeaponData->BulletProperties: %s"), WeaponData->BulletProperties ? TEXT("Valid") : TEXT("Null")); + } + } + + // Ensure weapon is visible + WeaponActor->SetActorHiddenInGame(false); + + if (WeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Weapon made visible")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] InternalApplyWeaponData() completed successfully")); + } + } + else + { + // Hide weapon when no data is present + WeaponActor->SetActorHiddenInGame(true); + + // Debug logging for null weapon data + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] InternalApplyWeaponData() called with null WeaponData - weapon hidden")); + } +} + +void UEBPlayerWeaponController::UpdateAmmoFromWeaponData() +{ + if (CurrentWeaponData) + { + // Set ammo if this is a new weapon or first time + if (CurrentAmmo == 0 && ReserveAmmo == 0) + { + CurrentAmmo = CurrentWeaponData->MagazineSize; + ReserveAmmo = CurrentWeaponData->MaxReserveAmmo; + OnAmmoChanged.Broadcast(CurrentAmmo, ReserveAmmo); + } + } +} + +void UEBPlayerWeaponController::HandleWeaponFired(UEBBulletPropertiesAsset* BulletType) +{ + if (HasAuthority()) + { + // Debug logging if enabled + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] HandleWeaponFired() called - Shot fired successfully!")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Bullet Type: %s"), BulletType ? *BulletType->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Pre-fire Ammo: %d"), CurrentAmmo); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Infinite Ammo: %s"), bInfiniteAmmo ? TEXT("True") : TEXT("False")); + } + + // Consume ammo + if (!bInfiniteAmmo && CurrentAmmo > 0) + { + CurrentAmmo--; + OnAmmoChanged.Broadcast(CurrentAmmo, ReserveAmmo); + + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Ammo consumed. New ammo count: %d"), CurrentAmmo); + } + } + + // Add heat from firing + if (CurrentWeaponData) + { + float PreviousHeat = CurrentWeaponData->GetHeatLevel(); + CurrentWeaponData->AddHeat(); + CurrentHeatLevel = CurrentWeaponData->GetHeatLevel(); + + if (CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Heat updated: %.2f%% -> %.2f%%"), PreviousHeat * 100.0f, CurrentHeatLevel * 100.0f); + if (CurrentWeaponData->IsOverheated()) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - WARNING: Weapon is now OVERHEATED!")); + } + } + } + + // Trigger VFX + if (bEnableAdvancedVFX) + { + TriggerMuzzleFlash(); + TriggerShellEjection(); + UpdateHeatEffects(); + + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - VFX triggered (muzzle flash, shell ejection, heat effects)")); + } + } + + // Check for empty + if (CurrentAmmo <= 0) + { + OnWeaponEmpty.Broadcast(CurrentWeaponData); + + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Weapon is now EMPTY! OnWeaponEmpty broadcast")); + } + } + + // Broadcast fired event + OnWeaponFired.Broadcast(CurrentWeaponData); + + if (CurrentWeaponData && CurrentWeaponData->bEnableFireDebug) + { + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - OnWeaponFired broadcast completed")); + UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - Final state: Ammo=%d, Heat=%.2f%%, Overheated=%s"), + CurrentAmmo, CurrentHeatLevel * 100.0f, CurrentWeaponData->IsOverheated() ? TEXT("Yes") : TEXT("No")); + } + } +} + +void UEBPlayerWeaponController::HandleMagazineEmpty() +{ + if (HasAuthority()) + { + OnWeaponEmpty.Broadcast(CurrentWeaponData); + } +} + +void UEBPlayerWeaponController::BindWeaponEvents() +{ + if (WeaponActor) + { + WeaponActor->OnGunFired.AddDynamic(this, &UEBPlayerWeaponController::HandleWeaponFired); + WeaponActor->OnMagazineEmpty.AddDynamic(this, &UEBPlayerWeaponController::HandleMagazineEmpty); + } +} + +void UEBPlayerWeaponController::UnbindWeaponEvents() +{ + if (WeaponActor) + { + WeaponActor->OnGunFired.RemoveDynamic(this, &UEBPlayerWeaponController::HandleWeaponFired); + WeaponActor->OnMagazineEmpty.RemoveDynamic(this, &UEBPlayerWeaponController::HandleMagazineEmpty); + } +} + +bool UEBPlayerWeaponController::HasAuthority() const +{ + AActor* Owner = GetOwner(); + return Owner ? Owner->HasAuthority() : false; +} + +// ======================================== +// SERVER RPCS +// ======================================== + +void UEBPlayerWeaponController::ServerSwitchToWeapon_Implementation(int32 WeaponIndex) +{ + SwitchToWeapon(WeaponIndex); +} + +void UEBPlayerWeaponController::ServerSwitchToWeaponData_Implementation(UEBWeaponData* WeaponData) +{ + SwitchToWeaponData(WeaponData); +} + +void UEBPlayerWeaponController::ServerAddWeapon_Implementation(UEBWeaponData* WeaponData) +{ + AddWeapon(WeaponData); +} + +void UEBPlayerWeaponController::ServerRemoveWeapon_Implementation(UEBWeaponData* WeaponData) +{ + RemoveWeapon(WeaponData); +} + +void UEBPlayerWeaponController::ServerSetCurrentAmmo_Implementation(int32 Amount) +{ + SetCurrentAmmo(Amount); +} + +void UEBPlayerWeaponController::ServerSetReserveAmmo_Implementation(int32 Amount) +{ + SetReserveAmmo(Amount); +} + +void UEBPlayerWeaponController::ServerToggleFireMode_Implementation() +{ + ToggleFireMode(); +} + +void UEBPlayerWeaponController::ServerToggleSafety_Implementation() +{ + ToggleSafety(); +} + +// ======================================== +// REPLICATION +// ======================================== + +void UEBPlayerWeaponController::OnRep_CurrentWeaponData() +{ + InternalApplyWeaponData(CurrentWeaponData); + OnWeaponChanged.Broadcast(CurrentWeaponData); +} + +void UEBPlayerWeaponController::OnRep_WeaponInventory() +{ + // Inventory changed, might need to update UI +} + +void UEBPlayerWeaponController::OnRep_CurrentWeaponIndex() +{ + // Weapon index changed, might need to update UI +} + +// ======================================== +// REPLICATION NOTIFICATIONS +// ======================================== + +void UEBPlayerWeaponController::OnRep_WeaponActor() +{ + if (WeaponActor) + { + // Ensure the weapon is attached correctly on the owning client + WeaponActor->AttachToComponent(this, FAttachmentTransformRules::SnapToTargetIncludingScale); + WeaponActor->SetActorRelativeTransform(WeaponSpawnOffset); + + // Configure collision to prevent collision with player + WeaponActor->SetActorEnableCollision(false); + + // Re-bind events (replication does not copy dynamic binds) + BindWeaponEvents(); + + // Apply the current weapon data if we already have one + if (CurrentWeaponData) + { + InternalApplyWeaponData(CurrentWeaponData); + } + } +} + +// ======================================== +// NIAGARA VFX IMPLEMENTATIONS +// ======================================== + +bool UEBPlayerWeaponController::IsOverheated() const +{ + return CurrentWeaponData ? CurrentWeaponData->IsOverheated() : false; +} + +void UEBPlayerWeaponController::PrintWeaponDebugInfo() const +{ + UE_LOG(LogTemp, Warning, TEXT("=== WEAPON DEBUG INFO ===")); + UE_LOG(LogTemp, Warning, TEXT("Controller State:")); + UE_LOG(LogTemp, Warning, TEXT("- Owner: %s"), GetOwner() ? *GetOwner()->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("- HasAuthority: %s"), HasAuthority() ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("- WeaponActor: %s"), WeaponActor ? *WeaponActor->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("- CurrentWeaponData: %s"), CurrentWeaponData ? *CurrentWeaponData->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("- CurrentWeaponIndex: %d"), CurrentWeaponIndex); + UE_LOG(LogTemp, Warning, TEXT("- WeaponInventory Count: %d"), WeaponInventory.Num()); + + UE_LOG(LogTemp, Warning, TEXT("Ammo State:")); + UE_LOG(LogTemp, Warning, TEXT("- CurrentAmmo: %d"), CurrentAmmo); + UE_LOG(LogTemp, Warning, TEXT("- ReserveAmmo: %d"), ReserveAmmo); + UE_LOG(LogTemp, Warning, TEXT("- InfiniteAmmo: %s"), bInfiniteAmmo ? TEXT("True") : TEXT("False")); + + if (CurrentWeaponData) + { + UE_LOG(LogTemp, Warning, TEXT("Current Weapon Data:")); + UE_LOG(LogTemp, Warning, TEXT("- Name: %s"), *CurrentWeaponData->WeaponName.ToString()); + UE_LOG(LogTemp, Warning, TEXT("- Fire Rate: %.2f RPM"), CurrentWeaponData->FireRate); + UE_LOG(LogTemp, Warning, TEXT("- Damage: %.2f"), CurrentWeaponData->Damage); + UE_LOG(LogTemp, Warning, TEXT("- Magazine Size: %d"), CurrentWeaponData->MagazineSize); + UE_LOG(LogTemp, Warning, TEXT("- Muzzle Velocity: %.2f cm/s"), CurrentWeaponData->MuzzleVelocity); + UE_LOG(LogTemp, Warning, TEXT("- Heat Level: %.2f%%"), CurrentWeaponData->GetHeatLevel() * 100.0f); + UE_LOG(LogTemp, Warning, TEXT("- Overheated: %s"), CurrentWeaponData->IsOverheated() ? TEXT("Yes") : TEXT("No")); + UE_LOG(LogTemp, Warning, TEXT("- Debug Enabled: %s"), CurrentWeaponData->bEnableFireDebug ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("- FirstPersonMesh: %s"), CurrentWeaponData->FirstPersonMesh ? *CurrentWeaponData->FirstPersonMesh->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("- ThirdPersonMesh: %s"), CurrentWeaponData->ThirdPersonMesh ? *CurrentWeaponData->ThirdPersonMesh->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("- StaticMesh: %s"), CurrentWeaponData->StaticMesh ? *CurrentWeaponData->StaticMesh->GetName() : TEXT("None")); + UE_LOG(LogTemp, Warning, TEXT("- BulletProperties: %s"), CurrentWeaponData->BulletProperties ? *CurrentWeaponData->BulletProperties->GetName() : TEXT("None")); + } + + if (WeaponActor) + { + UE_LOG(LogTemp, Warning, TEXT("Weapon Actor State:")); + UE_LOG(LogTemp, Warning, TEXT("- CanFire: %s"), WeaponActor->CanFire() ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("- Safety On: %s"), WeaponActor->bSafetyOn ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("- Hidden: %s"), WeaponActor->IsHidden() ? TEXT("True") : TEXT("False")); + UE_LOG(LogTemp, Warning, TEXT("- Magazine: %s"), WeaponActor->Magazine ? TEXT("Valid") : TEXT("None")); + + EFireMode CurrentFireMode = WeaponActor->GetFireMode(); + FString FireModeString = TEXT("Unknown"); + switch (CurrentFireMode) + { + case EFireMode::FM_Auto: FireModeString = TEXT("Auto"); break; + case EFireMode::FM_Semiauto: FireModeString = TEXT("Semi"); break; + case EFireMode::FM_Burst: FireModeString = TEXT("Burst"); break; + case EFireMode::FM_Manual: FireModeString = TEXT("Manual"); break; + case EFireMode::FM_InterBurst: FireModeString = TEXT("InterBurst"); break; + case EFireMode::FM_Slamfire: FireModeString = TEXT("Slamfire"); break; + case EFireMode::FM_Gatling: FireModeString = TEXT("Gatling"); break; + } + UE_LOG(LogTemp, Warning, TEXT("- Fire Mode: %s"), *FireModeString); + } + + UE_LOG(LogTemp, Warning, TEXT("CanFire() Result: %s"), CanFire() ? TEXT("TRUE") : TEXT("FALSE")); + UE_LOG(LogTemp, Warning, TEXT("=== END WEAPON DEBUG INFO ===")); +} + +void UEBPlayerWeaponController::TriggerMuzzleFlash() +{ + if (MuzzleFlashComponent && CurrentWeaponData && CurrentWeaponData->VFXConfig.MuzzleFlash) + { + // Use different effect if suppressed + UNiagaraSystem* EffectToUse = CurrentWeaponData->VFXConfig.MuzzleFlash; + if (CurrentWeaponData->VFXConfig.SuppressedMuzzleFlash) + { + // Check if weapon has suppressor (could add suppressor detection logic) + // EffectToUse = CurrentWeaponData->VFXConfig.SuppressedMuzzleFlash; + } + + MuzzleFlashComponent->SetAsset(EffectToUse); + ApplyNiagaraParameters(MuzzleFlashComponent, CurrentWeaponData->VFXConfig.MuzzleFlashParams); + MuzzleFlashComponent->Activate(true); + } +} + +void UEBPlayerWeaponController::TriggerShellEjection() +{ + if (WeaponActor && CurrentWeaponData && CurrentWeaponData->VFXConfig.EjectedShell) + { + FVector EjectionLocation = WeaponActor->GetMuzzleTransform().GetLocation(); + FVector EjectionVelocity = CurrentWeaponData->VFXConfig.ShellEjectionVelocity; + + UNiagaraFunctionLibrary::SpawnSystemAtLocation( + GetWorld(), + CurrentWeaponData->VFXConfig.EjectedShell, + EjectionLocation, + FRotator::ZeroRotator, + FVector::OneVector, + true, + true, + ENCPoolMethod::AutoRelease + ); + } +} + +void UEBPlayerWeaponController::SpawnTracerEffect(const FVector& StartLocation, const FVector& EndLocation) +{ + if (CurrentWeaponData && CurrentWeaponData->VFXConfig.TracerEffect) + { + UNiagaraComponent* TracerComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocation( + GetWorld(), + CurrentWeaponData->VFXConfig.TracerEffect, + StartLocation, + FRotator::ZeroRotator, + FVector::OneVector, + true, + true, + ENCPoolMethod::AutoRelease + ); + + if (TracerComponent) + { + // Set tracer end location + TracerComponent->SetVectorParameter(TEXT("EndLocation"), EndLocation); + + // Apply tracer parameters + ApplyNiagaraParameters(TracerComponent, CurrentWeaponData->VFXConfig.TracerParams); + } + } +} + +void UEBPlayerWeaponController::SpawnImpactEffect(const FVector& ImpactLocation, const FVector& ImpactNormal, UPhysicalMaterial* SurfaceMaterial) +{ + if (!CurrentWeaponData) + return; + + UNiagaraSystem* ImpactEffect = CurrentWeaponData->VFXConfig.HitImpact; + + // Check for material-specific impact effect + if (SurfaceMaterial && CurrentWeaponData->VFXConfig.MaterialImpactEffects.Contains(SurfaceMaterial)) + { + ImpactEffect = CurrentWeaponData->VFXConfig.MaterialImpactEffects[SurfaceMaterial]; + } + + if (ImpactEffect) + { + FRotator ImpactRotation = FRotationMatrix::MakeFromZ(ImpactNormal).Rotator(); + + UNiagaraComponent* ImpactComponent = UNiagaraFunctionLibrary::SpawnSystemAtLocation( + GetWorld(), + ImpactEffect, + ImpactLocation, + ImpactRotation, + FVector::OneVector, + true, + true, + ENCPoolMethod::AutoRelease + ); + + if (ImpactComponent) + { + // Set impact normal + ImpactComponent->SetVectorParameter(TEXT("ImpactNormal"), ImpactNormal); + + // Apply impact parameters + ApplyNiagaraParameters(ImpactComponent, CurrentWeaponData->VFXConfig.HitImpactParams); + } + } +} + +void UEBPlayerWeaponController::UpdateHeatEffects() +{ + if (BarrelHeatComponent && CurrentWeaponData && CurrentWeaponData->VFXConfig.BarrelHeatEffect) + { + float HeatLevel = CurrentWeaponData->GetHeatLevel(); + + // Update heat effect intensity based on heat level + BarrelHeatComponent->SetFloatParameter(TEXT("HeatLevel"), HeatLevel); + BarrelHeatComponent->SetFloatParameter(TEXT("Intensity"), HeatLevel * 2.0f); + + // Change effect visibility based on heat + if (HeatLevel > 0.3f && !BarrelHeatComponent->IsActive()) + { + BarrelHeatComponent->Activate(true); + } + else if (HeatLevel <= 0.1f && BarrelHeatComponent->IsActive()) + { + BarrelHeatComponent->Deactivate(); + } + } +} + +void UEBPlayerWeaponController::SetNiagaraFloatParameter(const FString& ParameterName, float Value) +{ + if (MuzzleFlashComponent) MuzzleFlashComponent->SetFloatParameter(*ParameterName, Value); + if (MuzzleSmokeComponent) MuzzleSmokeComponent->SetFloatParameter(*ParameterName, Value); + if (BarrelHeatComponent) BarrelHeatComponent->SetFloatParameter(*ParameterName, Value); + if (AttachmentGlowComponent) AttachmentGlowComponent->SetFloatParameter(*ParameterName, Value); +} + +void UEBPlayerWeaponController::SetNiagaraVectorParameter(const FString& ParameterName, const FVector& Value) +{ + if (MuzzleFlashComponent) MuzzleFlashComponent->SetVectorParameter(*ParameterName, Value); + if (MuzzleSmokeComponent) MuzzleSmokeComponent->SetVectorParameter(*ParameterName, Value); + if (BarrelHeatComponent) BarrelHeatComponent->SetVectorParameter(*ParameterName, Value); + if (AttachmentGlowComponent) AttachmentGlowComponent->SetVectorParameter(*ParameterName, Value); +} + +void UEBPlayerWeaponController::SetNiagaraColorParameter(const FString& ParameterName, const FLinearColor& Value) +{ + if (MuzzleFlashComponent) MuzzleFlashComponent->SetColorParameter(*ParameterName, Value); + if (MuzzleSmokeComponent) MuzzleSmokeComponent->SetColorParameter(*ParameterName, Value); + if (BarrelHeatComponent) BarrelHeatComponent->SetColorParameter(*ParameterName, Value); + if (AttachmentGlowComponent) AttachmentGlowComponent->SetColorParameter(*ParameterName, Value); +} + +void UEBPlayerWeaponController::CreateNiagaraComponents() +{ + if (!WeaponActor) + return; + + // Create persistent Niagara components attached to weapon + MuzzleFlashComponent = NewObject(WeaponActor); + MuzzleSmokeComponent = NewObject(WeaponActor); + BarrelHeatComponent = NewObject(WeaponActor); + AttachmentGlowComponent = NewObject(WeaponActor); + + // Attach to muzzle socket + if (MuzzleFlashComponent) + { + MuzzleFlashComponent->SetupAttachment(WeaponActor->SkeletalGunMesh, TEXT("MuzzleSocket")); + MuzzleFlashComponent->SetAutoActivate(false); + } + + if (MuzzleSmokeComponent) + { + MuzzleSmokeComponent->SetupAttachment(WeaponActor->SkeletalGunMesh, TEXT("MuzzleSocket")); + MuzzleSmokeComponent->SetAutoActivate(false); + } + + // Attach heat effect to barrel + if (BarrelHeatComponent) + { + BarrelHeatComponent->SetupAttachment(WeaponActor->SkeletalGunMesh, TEXT("BarrelSocket")); + BarrelHeatComponent->SetAutoActivate(false); + } + + // Attach glow effect to weapon body + if (AttachmentGlowComponent) + { + AttachmentGlowComponent->SetupAttachment(WeaponActor->SkeletalGunMesh); + AttachmentGlowComponent->SetAutoActivate(false); + } +} + +void UEBPlayerWeaponController::UpdateNiagaraComponents() +{ + if (!CurrentWeaponData || !bEnableAdvancedVFX) + return; + + // Update muzzle flash + if (MuzzleFlashComponent && CurrentWeaponData->VFXConfig.MuzzleFlash) + { + MuzzleFlashComponent->SetAsset(CurrentWeaponData->VFXConfig.MuzzleFlash); + ApplyNiagaraParameters(MuzzleFlashComponent, CurrentWeaponData->VFXConfig.MuzzleFlashParams); + } + + // Update muzzle smoke + if (MuzzleSmokeComponent && CurrentWeaponData->VFXConfig.MuzzleSmoke) + { + MuzzleSmokeComponent->SetAsset(CurrentWeaponData->VFXConfig.MuzzleSmoke); + ApplyNiagaraParameters(MuzzleSmokeComponent, CurrentWeaponData->VFXConfig.MuzzleSmokeParams); + } + + // Update barrel heat + if (BarrelHeatComponent && CurrentWeaponData->VFXConfig.BarrelHeatEffect) + { + BarrelHeatComponent->SetAsset(CurrentWeaponData->VFXConfig.BarrelHeatEffect); + } + + // Update attachment glow + if (AttachmentGlowComponent && CurrentWeaponData->VFXConfig.AttachmentGlowEffect) + { + AttachmentGlowComponent->SetAsset(CurrentWeaponData->VFXConfig.AttachmentGlowEffect); + } + + // Apply global parameters + ApplyNiagaraParameters(MuzzleFlashComponent, CurrentWeaponData->GlobalEffectParams); + ApplyNiagaraParameters(MuzzleSmokeComponent, CurrentWeaponData->GlobalEffectParams); + ApplyNiagaraParameters(BarrelHeatComponent, CurrentWeaponData->GlobalEffectParams); + ApplyNiagaraParameters(AttachmentGlowComponent, CurrentWeaponData->GlobalEffectParams); +} + +void UEBPlayerWeaponController::ApplyNiagaraParameters(UNiagaraComponent* Component, const FNiagaraEffectParams& Params) +{ + if (!Component) + return; + + // Apply basic parameters + Component->SetFloatParameter(TEXT("Intensity"), Params.Intensity); + Component->SetFloatParameter(TEXT("Scale"), Params.Scale); + Component->SetColorParameter(TEXT("ColorTint"), Params.ColorTint); + + // Apply custom parameters + for (const auto& FloatParam : Params.FloatParameters) + { + Component->SetFloatParameter(*FloatParam.Key, FloatParam.Value); + } + + for (const auto& VectorParam : Params.VectorParameters) + { + Component->SetVectorParameter(*VectorParam.Key, VectorParam.Value); + } + + for (const auto& BoolParam : Params.BoolParameters) + { + Component->SetBoolParameter(*BoolParam.Key, BoolParam.Value); + } +} + +void UEBPlayerWeaponController::UpdateWeaponHeat(float DeltaTime) +{ + if (CurrentWeaponData && HasAuthority()) + { + CurrentWeaponData->UpdateHeat(DeltaTime); + CurrentHeatLevel = CurrentWeaponData->GetHeatLevel(); + + // Update heat effects if enabled + if (bEnableAdvancedVFX) + { + UpdateHeatEffects(); + } + } +} \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBWeaponData.cpp b/Source/EasyBallistics/Private/EBWeaponData.cpp new file mode 100644 index 0000000..1f70fe4 --- /dev/null +++ b/Source/EasyBallistics/Private/EBWeaponData.cpp @@ -0,0 +1,187 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#include "EBWeaponData.h" +#include "EBBarrel.h" + +UEBWeaponData::UEBWeaponData() +{ + // Set default values + WeaponName = FText::FromString(TEXT("Weapon")); + WeaponDescription = FText::FromString(TEXT("A standard weapon")); + WeaponType = EWeaponType::Rifle; + WeaponIcon = nullptr; + WeaponTier = 1; + + // Visuals + FirstPersonMesh = nullptr; + ThirdPersonMesh = nullptr; + StaticMesh = nullptr; + + // Ballistics + BulletProperties = nullptr; + Damage = 30.0f; + FireRate = 600.0f; + MuzzleVelocity = 91440.0f; + Accuracy = 0.95f; + RecoilStrength = 1.0f; + EffectiveRange = 300.0f; + + // Magazine + MagazineSize = 30; + MaxReserveAmmo = 120; + ReloadTime = 2.5f; + TacticalReloadTime = 2.0f; + + // Fire modes + AvailableFireModes.Add(EFireMode::FM_Auto); + AvailableFireModes.Add(EFireMode::FM_Semiauto); + DefaultFireMode = EFireMode::FM_Auto; + BurstCount = 3; + + // Attachments + MuzzleSocketName = TEXT("MuzzleSocket"); + ScopeSocketName = TEXT("ScopeSocket"); + GripSocketName = TEXT("GripSocket"); + LaserSocketName = TEXT("LaserSocket"); + + // Advanced + WeaponWeight = 3.5f; + ADSTime = 0.3f; + SprintToFireTime = 0.25f; + EquipTime = 0.75f; + bCanDualWield = false; + bHasSafety = true; + bSuppressorCompatible = true; + + // Niagara advanced features + bUseGPUSimulation = true; + bEnableEffectLOD = true; + WeaponDataInterface = nullptr; + MaxHeatLevel = 100.0f; + HeatDecayRate = 10.0f; + HeatPerShot = 5.0f; + CurrentHeat = 0.0f; + + // Debug settings + bEnableFireDebug = false; + DebugArrowSize = 100.0f; + bDebugImpactInfo = false; + bDebugTrajectory = false; + bDebugPhysics = false; + bDebugPerformance = false; + bDebugBallistics = false; + bDebugSpalling = false; +} + +FString UEBWeaponData::GetWeaponTypeString() const +{ + switch (WeaponType) + { + case EWeaponType::Rifle: return TEXT("Rifle"); + case EWeaponType::Pistol: return TEXT("Pistol"); + case EWeaponType::Shotgun: return TEXT("Shotgun"); + case EWeaponType::SMG: return TEXT("SMG"); + case EWeaponType::LMG: return TEXT("LMG"); + case EWeaponType::Sniper: return TEXT("Sniper"); + case EWeaponType::Launcher: return TEXT("Launcher"); + case EWeaponType::Melee: return TEXT("Melee"); + case EWeaponType::Special: return TEXT("Special"); + default: return TEXT("Unknown"); + } +} + +void UEBWeaponData::AddHeat(float Amount) +{ + float HeatToAdd = (Amount < 0.0f) ? HeatPerShot : Amount; + CurrentHeat = FMath::Clamp(CurrentHeat + HeatToAdd, 0.0f, MaxHeatLevel); +} + +void UEBWeaponData::UpdateHeat(float DeltaTime) +{ + if (CurrentHeat > 0.0f) + { + CurrentHeat = FMath::Max(0.0f, CurrentHeat - (HeatDecayRate * DeltaTime)); + } +} + +// ======================================== +// DEBUG UTILITY FUNCTIONS +// ======================================== + +void UEBWeaponData::SetBulletDebugCategory(const FString& CategoryName, bool bEnabled) +{ + if (CategoryName == TEXT("Trajectory")) + { + bDebugTrajectory = bEnabled; + } + else if (CategoryName == TEXT("Impact")) + { + bDebugImpactInfo = bEnabled; + } + else if (CategoryName == TEXT("Physics")) + { + bDebugPhysics = bEnabled; + } + else if (CategoryName == TEXT("Performance")) + { + bDebugPerformance = bEnabled; + } + else if (CategoryName == TEXT("Ballistics")) + { + bDebugBallistics = bEnabled; + } + else if (CategoryName == TEXT("Spalling")) + { + bDebugSpalling = bEnabled; + } +} + +void UEBWeaponData::SetAllBulletDebugCategories(bool bEnabled) +{ + bDebugTrajectory = bEnabled; + bDebugImpactInfo = bEnabled; + bDebugPhysics = bEnabled; + bDebugPerformance = bEnabled; + bDebugBallistics = bEnabled; + bDebugSpalling = bEnabled; +} + +FString UEBWeaponData::GetBulletDebugInfo() const +{ + TArray EnabledCategories; + + if (bDebugTrajectory) EnabledCategories.Add(TEXT("Trajectory")); + if (bDebugImpactInfo) EnabledCategories.Add(TEXT("Impact")); + if (bDebugPhysics) EnabledCategories.Add(TEXT("Physics")); + if (bDebugPerformance) EnabledCategories.Add(TEXT("Performance")); + if (bDebugBallistics) EnabledCategories.Add(TEXT("Ballistics")); + if (bDebugSpalling) EnabledCategories.Add(TEXT("Spalling")); + + if (EnabledCategories.Num() == 0) + { + return TEXT("No debug categories enabled"); + } + + return FString::Printf(TEXT("Enabled Debug Categories: %s | Arrow Size: %.0f"), + *FString::Join(EnabledCategories, TEXT(", ")), DebugArrowSize); +} + +void UEBWeaponData::ApplyDebugSettingsToBarrel(class UEBBarrel* Barrel) const +{ + if (!Barrel) + { + return; + } + + // Apply debug arrow size + Barrel->DebugArrowSize = DebugArrowSize; + Barrel->DebugImpactInfo = bDebugImpactInfo; + + // Apply debug categories through barrel's functions + Barrel->SetBulletDebugCategory(TEXT("Trajectory"), bDebugTrajectory); + Barrel->SetBulletDebugCategory(TEXT("Impact"), bDebugImpactInfo); + Barrel->SetBulletDebugCategory(TEXT("Physics"), bDebugPhysics); + Barrel->SetBulletDebugCategory(TEXT("Performance"), bDebugPerformance); + Barrel->SetBulletDebugCategory(TEXT("Ballistics"), bDebugBallistics); + Barrel->SetBulletDebugCategory(TEXT("Spalling"), bDebugSpalling); +} \ No newline at end of file diff --git a/Source/EasyBallistics/Private/Reload.cpp b/Source/EasyBallistics/Private/Reload.cpp index 9ecb2f5..3253e6c 100644 --- a/Source/EasyBallistics/Private/Reload.cpp +++ b/Source/EasyBallistics/Private/Reload.cpp @@ -1,74 +1,4 @@ // Copyright 2016 Mookie. All Rights Reserved. -#include "EBBarrel.h" -#include "EBBullet.h" - -TArray> UEBBarrel::GetAmmo(bool CountChambered) const { - if (!CountChambered || ChamberedBullet == nullptr) { - return Ammo; - } - else { - TArray> RetAmmo; - RetAmmo.Add(ChamberedBullet); - RetAmmo.Append(Ammo); - return RetAmmo; - }; -}; - -int UEBBarrel::GetAmmoCount(bool CountChambered) const { - - int remainingAmmo; - if (CycleAmmo) { - remainingAmmo = CycleAmmoCount; - } - else { - remainingAmmo = Ammo.Num(); - }; - - if (CountChambered) { - if (ChamberedBullet != nullptr) { - remainingAmmo++; - }; - }; - - return remainingAmmo; -}; - -void UEBBarrel::SetAmmo(int Count, bool UnloadChambered, bool CancelShooting, bool ManualCharge, const TArray>& NewAmmo) { - Ammo = NewAmmo; - - CycleAmmoCount = Count; - - if (UnloadChambered) { - ChamberedBullet = nullptr; - }; - - if (CancelShooting) { - BurstRemaining = 0; - Shooting = false; - }; - - if (ManualCharge) { - LoadNext = false; - }; -}; - -void UEBBarrel::Charge_Implementation() { - LoadNext = true; -}; - -bool UEBBarrel::Charge_Validate() { - return true; -}; - -void UEBBarrel::UnloadChambered_Implementation(bool ManualCharge) { - ChamberedBullet = nullptr; - - if (ManualCharge) { - LoadNext = false; - }; -}; - -bool UEBBarrel::UnloadChambered_Validate(bool ManualCharge) { - return true; -}; \ No newline at end of file +// This file contained legacy reloading and ammo management logic that has been moved to AEBGun and UEBMagazine. +// The contents have been cleared to resolve compilation errors after a major refactoring. \ No newline at end of file diff --git a/Source/EasyBallistics/Private/Trace.cpp b/Source/EasyBallistics/Private/Trace.cpp index 9fc69ab..d0f1e7e 100644 --- a/Source/EasyBallistics/Private/Trace.cpp +++ b/Source/EasyBallistics/Private/Trace.cpp @@ -306,11 +306,18 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn // Create debug impact widget if enabled CreateDebugImpactWidget(HitResult.Location, Ricochet, Penetration, Velocity, PenetrationDepth, PhysMaterial); + bool bDidSpallThisImpact = false; // Generate spalling fragments if conditions are met if (HasAuthority() && ShouldGenerateSpalling(Velocity, PhysMaterial)) { + bDidSpallThisImpact = true; // Entry spalling - GenerateSpallFragments(HitResult.Location, Velocity, HitResult.Normal, PhysMaterial, HitResult.GetActor()); + { + float EntryImpactAngleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Abs(FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal)))); + GenerateSpallFragments(HitResult.Location, Velocity, HitResult.Normal, + 0.0f, EntryImpactAngleDeg, + PhysMaterial, HitResult.GetActor()); + } // Exit spalling (backspall) for penetration - typically more dangerous if (Penetration && (exitLoc - HitResult.Location).Size() > 1.0f) @@ -318,10 +325,21 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn // Exit spalling velocity is based on remaining velocity after penetration // Backspall typically has higher fragment velocity than entry spalling FVector ExitSpallVelocity = NewVelocity * 1.2f; // Amplify for more realistic backspall effect - GenerateSpallFragments(exitLoc, ExitSpallVelocity, -exitNormal, PhysMaterial, HitResult.GetActor()); + { + float ExitImpactAngleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Abs(FVector::DotProduct(NewVelocity.GetSafeNormal(), -exitNormal)))); + GenerateSpallFragments(exitLoc, ExitSpallVelocity, -exitNormal, + PenetrationDepth, ExitImpactAngleDeg, + PhysMaterial, HitResult.GetActor()); + } } } + if (bDidSpallThisImpact) + { + bHasSpalled = true; + NewVelocity = FVector::ZeroVector; + } + // SAFETY: Clamp velocity to prevent infinite speeds and numerical instability float MaxSafeVelocity = MuzzleVelocityMax * 10.0f; // Allow up to 10x muzzle velocity as safety limit if (!NewVelocity.IsNearlyZero() && NewVelocity.Size() > MaxSafeVelocity) diff --git a/Source/EasyBallistics/Public/EBBarrel.h b/Source/EasyBallistics/Public/EBBarrel.h index f099443..382c139 100644 --- a/Source/EasyBallistics/Public/EBBarrel.h +++ b/Source/EasyBallistics/Public/EBBarrel.h @@ -97,54 +97,17 @@ public: UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Additional Spread bias, higher is more accurate on average", ClampMin = "0")) float SpreadBias = 0.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum of random multiplier applied to bullet muzzle velocity")) float MuzzleVelocityMultiplierMin = 1.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Maximum of random multiplier applied to bullet muzzle velocity")) float MuzzleVelocityMultiplierMax = 1.0f; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum fire rate, rounds per second")) float FireRateMin = 1.0f; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Maximum fire rate, rounds per second, set to same number as FireRateMin to disable randomization")) float FireRateMax = 1.0f; - UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon") EFireMode FireMode = EFireMode::FM_Auto; - UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon") bool ShootingBlocked; - - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Number of rounds auto fired in burst mode")) int BurstCount = 3; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum time between bursts")) float BurstCooldown = 0.0f; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Automatically spin up gatling when trigger is being held down")) bool GatlingAutoSpool = true; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon") float GatlingSpoolUpTime = 1.0f; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon") float GatlingSpoolDownTime = 1.0f; - UPROPERTY(BlueprintReadWrite, Category = "Weapon") float GatlingPhase = 0.0f; - - // Legacy Ammo System (kept for compatibility, use Gun's Magazine system for new weapons) - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons")) - bool CycleAmmo = false; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (EditCondition = "CycleAmmo", ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons")) - bool CycleAmmoUnlimited = true; - UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons")) - TArray> Ammo; - UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (EditCondition = "CycleAmmo", ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons")) - int CycleAmmoCount; - UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (EditCondition = "CycleAmmo", ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons")) - int CycleAmmoPos; - - UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") TSubclassOf ChamberedBullet; - UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") bool Shooting; - UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") bool Spooling = false; - UPROPERTY(BlueprintReadWrite, Category = "Weapon") float GatlingRPS = 0.0f; - - UPROPERTY(BlueprintReadWrite, Category = "WeaponState") bool LoadNext=true; - UPROPERTY(BlueprintReadWrite, Category = "WeaponState") float Cooldown; - UPROPERTY(BlueprintReadWrite, Category = "WeaponState") int BurstRemaining; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateVariables=true; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateShotFiredEvents = true; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ClientSideAim=false; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") float ClientAimUpdateFrequency = 15.0f; - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") float ClientAimDistanceLimit = 200.0f; FRandomStream RandomStream; - // Legacy Ammo Functions (kept for compatibility) - UFUNCTION() void NextBullet(); - UFUNCTION(BlueprintPure, Category = "Legacy Ammo") int GetAmmoCount(bool CountChambered) const; - UFUNCTION(BlueprintPure, Category = "Legacy Ammo") TArray> GetAmmo(bool CountChambered) const; - UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "Legacy Ammo") void SetAmmo(int count, bool UnloadChambered, bool CancelShooting, bool ManualCharge, const TArray>& NewAmmo); - UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Legacy Ammo") void Charge(); - UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Legacy Ammo") void UnloadChambered(bool ManualCharge); + // New simplified single-shot firing interface. Spawns exactly one bullet of the given class using + // the barrel's current transform. This bypasses the legacy internal shooting state machine so that + // higher-level gun/weapon controllers can directly fire a round without manipulating Barrel internals. + UFUNCTION(BlueprintCallable, Category = "Shooting") + void FireBullet(TSubclassOf BulletClass); // New Gun-Integrated Ammo Functions UFUNCTION(BlueprintPure, Category = "Gun Integration") @@ -155,9 +118,6 @@ public: UFUNCTION(BlueprintPure, Category = "Gun Integration") bool CanFireFromGun() const; - UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Shooting") void SwitchFireMode(EFireMode NewFireMode); - UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Shooting") void GatlingSpool(bool Spool); - UFUNCTION(BlueprintCallable, Category = "Shooting") void Shoot(bool Trigger); UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHit(bool& Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray& Trajectory, TSubclassOf BulletClass, TArrayIgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const; UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHitFromLocation(bool &Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray& Trajectory, TSubclassOf BulletClass, FVector StartLocation, FVector AimDirection, TArrayIgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const; diff --git a/Source/EasyBallistics/Public/EBBullet.h b/Source/EasyBallistics/Public/EBBullet.h index e169ac7..d5b777f 100644 --- a/Source/EasyBallistics/Public/EBBullet.h +++ b/Source/EasyBallistics/Public/EBBullet.h @@ -11,6 +11,7 @@ #include "Kismet/GameplayStatics.h" #include "DrawDebugHelpers.h" #include "Components/PrimitiveComponent.h" +#include "Components/SphereComponent.h" #include "EBMaterialResponseMap.h" #include "EBBulletProperties.h" @@ -38,6 +39,10 @@ public: // Sets default values for this actor's properties AEBBullet(); + // Collision Component + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Collision") + class USphereComponent* Collision; + UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FVector Velocity; UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FRandomStream RandomStream; @@ -219,7 +224,15 @@ public: UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Enable spalling fragment generation on impact")) bool EnableSpalling = true; - UPROPERTY(BlueprintReadWrite, Category = "Impact", meta = (ToolTip = "Indicates if this bullet is a spalling fragment")) + /** Flag to indicate if this bullet has already generated spall, preventing repeated spalling. */ + UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "Spalling") + bool bHasSpalled; + + // Spalling properties (can be overridden by Material Response Map) + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spalling") + TSubclassOf SpallFragmentClass; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Indicates if this bullet is a spalling fragment")) bool IsSpallFragment = false; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Replication") bool ReliableReplication = false; @@ -346,7 +359,19 @@ public: // Spalling Functions UFUNCTION(BlueprintCallable, Category = "EBBullet|Spalling") - void GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, UPhysicalMaterial* Material, AActor* HitActor); + /** + * Generate spall fragments given impact parameters. + * @param ImpactLocation Location of impact in world space + * @param ImpactVelocity Incoming projectile velocity at impact (cm/s) + * @param ImpactNormal Surface normal at point of impact (unit vector) + * @param PlateThicknessCM Thickness of the target plate in centimetres (0 if unknown / no penetration) + * @param ImpactAngleDegrees Angle between velocity vector and surface normal in degrees (0 = perpendicular) + * @param Material Physical material hit + * @param HitActor Actor that was hit (for ignore lists etc.) + */ + void GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, + float PlateThicknessCM, float ImpactAngleDegrees, + UPhysicalMaterial* Material, AActor* HitActor); UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|Spalling") void OnSpallFragmentGenerated(FVector FragmentLocation, FVector FragmentVelocity, float FragmentMass, UPhysicalMaterial* Material); @@ -367,6 +392,11 @@ public: UFUNCTION(NetMulticast, Reliable) void DeactivationBroadcast(); private: + /** + * Returns a scaling factor (>1 increases spall likelihood/amount, <1 decreases) based on the bullet type. + */ + float GetSpallFactorByBulletType() const; + UPROPERTY() TArray> Pooled; static AEBBullet* GetFromPool(UWorld* World, UClass* BulletClass); static AEBBullet* SpawnOrReactivate(UWorld* World, TSubclassOf BulletClass, const FTransform& Transform, FVector BulletVelocity, AActor* BulletOwner, APawn* BulletInstigator); diff --git a/Source/EasyBallistics/Public/EBBulletProperties.h b/Source/EasyBallistics/Public/EBBulletProperties.h index 115bc25..ce5c675 100644 --- a/Source/EasyBallistics/Public/EBBulletProperties.h +++ b/Source/EasyBallistics/Public/EBBulletProperties.h @@ -199,6 +199,19 @@ struct FMathematicalMaterialProperties UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spalling Properties", meta = (EditCondition = "EnableMathematicalSpalling", ToolTip = "Maximum fragment count per unit area")) float MaxFragmentDensity = 50.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Poisson ratio (dimensionless)")) + float PoissonRatio = 0.3f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Longitudinal wave speed in the material (m/s)")) + float WaveSpeedMPerS = 5900.f; // steel approx + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Ultimate tensile strength MPa")) + float UltimateTensileStrengthMPa = 500.f; + + // Backing liner efficiency (0-1) for spall liner or composite backing. 1 = no liner, 0 = perfect absorption + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Efficiency factor of any backing liner (1.0 = none, lower = more spall reduction)")) + float BackingLinerEfficiency = 1.0f; + // Constructor with default steel properties FMathematicalMaterialProperties() { @@ -226,6 +239,9 @@ class EASYBALLISTICS_API UEBBulletPropertiesAsset : public UDataAsset GENERATED_BODY() public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning", meta = (ToolTip = "The bullet actor to spawn.")) + TSubclassOf BulletClass; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bullet Properties") FMathematicalBulletProperties BulletProperties; diff --git a/Source/EasyBallistics/Public/EBGun.h b/Source/EasyBallistics/Public/EBGun.h index 120a834..c8e68d1 100644 --- a/Source/EasyBallistics/Public/EBGun.h +++ b/Source/EasyBallistics/Public/EBGun.h @@ -26,6 +26,54 @@ enum class EGunState : uint8 GS_Malfunction UMETA(DisplayName = "Malfunction") }; +/** + * EasyBallistics Gun Actor - Complete weapon system with organized settings + * + * PROPERTY ORGANIZATION: + * 1. Configuration - Weapon config asset + * 2. Barrel & Ballistics - Essential shooting properties (START HERE) + * 3. Fire Control - Fire modes, rates, burst settings + * 4. Advanced Ballistics - Fine-tuning for realism + * 5. Gatling Settings - For minigun/gatling type weapons + * 6. Gun State - Read-only operational status + * 7. Firing State - Read-only runtime firing status + * + * BLUEPRINT NODE ORGANIZATION: + * All nodes are under "Ballistics" with logical subcategories: + * + * Ballistics| + * ├── Firing| + * │ ├── Trigger (PullTrigger, ReleaseTrigger) + * │ ├── Advanced (Shoot) + * │ ├── Gatling (GatlingSpool) + * │ └── Bullet Spawn (SpawnBullet) + * ├── Controls| + * │ ├── Safety (SetSafety) + * │ └── Action (CockHammer, ReleaseHammer) + * ├── Ammunition| + * │ ├── Magazine (LoadMagazine, EjectMagazine, InsertMagazine) + * │ └── Chamber (ChargingHandle, EjectChamberedRound) + * ├── Status| + * │ ├── Readiness (CanFire, IsLoaded) + * │ └── Ammunition (GetRoundsInMagazine, GetTotalRounds, GetChamberedBulletType) + * ├── Configuration| + * │ ├── Setup (ApplyWeaponConfiguration) + * │ ├── Fire Control (SetFireMode, GetFireMode, SwitchFireMode) + * │ └── Mesh (SetMeshType, GetActiveMeshComponent, GetMuzzleTransform, SetMuzzleSocketName) + * ├── Calculations| + * │ ├── Properties (GetBarrelLength, GetEffectiveMuzzleVelocity, GetBarrelAccuracy) + * │ └── Recoil (CalculateRecoilImpulse) + * ├── Prediction| + * │ ├── Trajectory (PredictHit) + * │ └── Targeting (CalculateAimDirection) + * ├── Events| + * │ ├── State (OnGunStateChanged) + * │ ├── Firing (OnGunFired) + * │ ├── Ammunition (OnMagazineEmpty, OnMagazineChanged) + * │ └── Safety (OnSafetyChanged) + * └── Legacy| + * └── Barrel (IsBarrelConnected) + */ UCLASS(Blueprintable, BlueprintType) class EASYBALLISTICS_API AEBGun : public AActor { @@ -34,135 +82,315 @@ class EASYBALLISTICS_API AEBGun : public AActor public: AEBGun(); - // Core Components + // Core Components - Gun Mesh (can be either skeletal or static) UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") - USkeletalMeshComponent* GunMesh; + USkeletalMeshComponent* SkeletalGunMesh; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") + UStaticMeshComponent* StaticGunMesh; + + // Gun Type Configuration + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components", meta = (ToolTip = "Use skeletal mesh (true) or static mesh (false). Set this to false to use StaticGunMesh.")) + bool bUseSkeletalMesh = true; + + // Firing system integrated into gun (replacing barrel dependency) + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gun Configuration", meta = (EditCondition = "!bUseSkeletalMesh", ToolTip = "Muzzle transform for static mesh mode")) + FTransform MuzzleTransform; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gun Configuration", meta = (EditCondition = "bUseSkeletalMesh", ToolTip = "Socket name for muzzle in skeletal mesh mode")) + FString MuzzleSocketName = TEXT("MuzzleSocket"); + + // Legacy barrel component for backward compatibility + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (ToolTip = "Legacy barrel component - firing logic now handled by gun")) UEBBarrel* Barrel; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") UEBMagazine* Magazine; - // Weapon Configuration - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Configuration") + // ======================================== + // MESH SETUP (Configure First) + // ======================================== + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (ToolTip = "Use skeletal mesh (true) or static mesh (false)")) + bool bUseSkeletal = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "bUseSkeletal", ToolTip = "Skeletal mesh for the gun")) + USkeletalMesh* SkeletalMeshAsset; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "!bUseSkeletal", ToolTip = "Static mesh for the gun")) + UStaticMesh* StaticMeshAsset; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "bUseSkeletal", ToolTip = "Socket name for muzzle in skeletal mesh mode")) + FString MuzzleSocket = TEXT("MuzzleSocket"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ballistics|Calculations|Recoil", meta = (ToolTip = "Can process next shot based on current state")) + bool bCanProcessNextShot = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "!bUseSkeletal", ToolTip = "Muzzle transform for static mesh mode")) + FTransform MuzzleOffset = FTransform::Identity; + + // ======================================== + // CONFIGURATION + // ======================================== + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "1. Configuration") UEBWeaponConfiguration* WeaponConfig; - // Gun State - UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_GunState, Category = "Gun State") + // ======================================== + // BARREL & BALLISTICS (Most Common Settings) + // ======================================== + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "1.0", ClampMax = "50.0", ToolTip = "Barrel length in inches")) + float BarrelLength = 16.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "10000.0", ClampMax = "200000.0", ToolTip = "Minimum muzzle velocity in cm/s")) + float MuzzleVelocityMin = 91440.0f; // ~3000 fps + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "10000.0", ClampMax = "200000.0", ToolTip = "Maximum muzzle velocity in cm/s")) + float MuzzleVelocityMax = 91440.0f; // ~3000 fps + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "0.0", ClampMax = "1.0", ToolTip = "Spread in radians - 0 = perfect accuracy")) + float Spread = 0.0f; + + // ======================================== + // FIRE CONTROL (Essential Settings) + // ======================================== + UPROPERTY(Replicated, EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control") + EFireMode FireMode = EFireMode::FM_Auto; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "0.1", ClampMax = "50.0", ToolTip = "Fire rate in rounds per second")) + float FireRateMin = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "0.1", ClampMax = "50.0", ToolTip = "Fire rate in rounds per second")) + float FireRateMax = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "1", ClampMax = "20", ToolTip = "Number of rounds per burst")) + int32 BurstCount = 3; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "0.0", ClampMax = "10.0", ToolTip = "Cooldown between bursts in seconds")) + float BurstCooldown = 0.0f; + + // ======================================== + // ADVANCED BALLISTICS (Fine-tuning) + // ======================================== + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "1.0", ClampMax = "20.0", ToolTip = "Rifling twist rate (1:X)")) + float RiflingTwist = 7.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.05", ClampMax = "1.0", ToolTip = "Bore radius in inches")) + float BoreRadius = 0.112f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.0", ClampMax = "10.0", ToolTip = "Spread bias - higher = more accurate on average")) + float SpreadBias = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.5", ClampMax = "2.0", ToolTip = "Minimum velocity multiplier")) + float MuzzleVelocityMultiplierMin = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.5", ClampMax = "2.0", ToolTip = "Maximum velocity multiplier")) + float MuzzleVelocityMultiplierMax = 1.0f; + + // ======================================== + // GATLING/MINIGUN (Specialized Settings) + // ======================================== + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "5. Gatling Settings", meta = (ToolTip = "Automatically spool up when trigger held")) + bool bGatlingAutoSpool = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "5. Gatling Settings", meta = (ClampMin = "0.1", ClampMax = "10.0", ToolTip = "Time to spool up in seconds")) + float GatlingSpoolUpTime = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "5. Gatling Settings", meta = (ClampMin = "0.1", ClampMax = "10.0", ToolTip = "Time to spool down in seconds")) + float GatlingSpoolDownTime = 1.0f; + + UPROPERTY(BlueprintReadOnly, Category = "5. Gatling Settings", meta = (ToolTip = "Current gatling phase (0-1)")) + float GatlingPhase = 0.0f; + + // ======================================== + // GUN STATE (Read-Only Status) + // ======================================== + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_GunState, Category = "6. Gun State", meta = (ToolTip = "Current operational state of the gun")) EGunState CurrentGunState = EGunState::GS_Ready; - UPROPERTY(BlueprintReadOnly, Replicated, Category = "Gun State") + UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Is the safety engaged")) bool bSafetyOn = false; - UPROPERTY(BlueprintReadOnly, Replicated, Category = "Gun State") + UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Is there a round chambered")) bool bChamberedRound = false; - UPROPERTY(BlueprintReadOnly, Replicated, Category = "Gun State") + UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Is the hammer cocked")) bool bHammerCocked = false; - // Firing Interface - UFUNCTION(BlueprintCallable, Category = "Gun|Firing") + // Chambered Round + UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Current bullet type in chamber")) + UEBBulletPropertiesAsset* ChamberedBulletType; + + // ======================================== + // FIRING STATE (Runtime Status) + // ======================================== + UPROPERTY(BlueprintReadOnly, Replicated, Category = "7. Firing State", meta = (ToolTip = "Is currently shooting")) + bool bShooting = false; + + UPROPERTY(BlueprintReadOnly, Replicated, Category = "7. Firing State", meta = (ToolTip = "Is gatling gun spooling")) + bool bSpooling = false; + + UPROPERTY(BlueprintReadOnly, Category = "7. Firing State", meta = (ToolTip = "Current gatling rounds per second")) + float GatlingRPS = 0.0f; + + UPROPERTY(BlueprintReadOnly, Category = "7. Firing State", meta = (ToolTip = "Current cooldown remaining")) + float Cooldown = 0.0f; + + UPROPERTY(BlueprintReadOnly, Category = "7. Firing State", meta = (ToolTip = "Burst shots remaining")) + int32 BurstRemaining = 0; + + // ======================================== + // BLUEPRINT FUNCTIONS + // ======================================== + + // Primary Firing Interface + UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Trigger") void PullTrigger(); - UFUNCTION(BlueprintCallable, Category = "Gun|Firing") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Trigger") void ReleaseTrigger(); - UFUNCTION(BlueprintCallable, Category = "Gun|Controls") + // Gun Controls + UFUNCTION(BlueprintCallable, Category = "Ballistics|Controls|Safety") void SetSafety(bool bNewSafetyState); - UFUNCTION(BlueprintCallable, Category = "Gun|Controls") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Controls|Action") void CockHammer(); - UFUNCTION(BlueprintCallable, Category = "Gun|Controls") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Controls|Action") void ReleaseHammer(); // Ammunition Management - UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Magazine") void LoadMagazine(TArray BulletTypes); - UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Magazine") void EjectMagazine(); - UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Magazine") void InsertMagazine(UEBMagazine* NewMagazine); - UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Chamber") void ChargingHandle(); - UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Chamber") void EjectChamberedRound(); // State Queries - UFUNCTION(BlueprintPure, Category = "Gun|State") + UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Readiness") bool CanFire() const; - UFUNCTION(BlueprintPure, Category = "Gun|State") + UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Readiness") bool IsLoaded() const; - UFUNCTION(BlueprintPure, Category = "Gun|State") + UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Ammunition") int32 GetRoundsInMagazine() const; - UFUNCTION(BlueprintPure, Category = "Gun|State") + UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Ammunition") int32 GetTotalRounds() const; - UFUNCTION(BlueprintPure, Category = "Gun|State") + UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Ammunition") UEBBulletPropertiesAsset* GetChamberedBulletType() const; // Configuration - UFUNCTION(BlueprintCallable, Category = "Gun|Configuration") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Setup") void ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig); - UFUNCTION(BlueprintCallable, Category = "Gun|Configuration") + UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Fire Control") void SetFireMode(EFireMode NewFireMode); - UFUNCTION(BlueprintPure, Category = "Gun|Configuration") + UFUNCTION(BlueprintPure, Category = "Ballistics|Configuration|Fire Control") EFireMode GetFireMode() const; - // Barrel Integration - UFUNCTION(BlueprintPure, Category = "Gun|Barrel") + // Gun Mesh Management + UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Mesh") + void SetMeshType(bool bUseSkeletalMeshParam); + + UFUNCTION(BlueprintPure, Category = "Ballistics|Configuration|Mesh") + UPrimitiveComponent* GetActiveMeshComponent() const; + + UFUNCTION(BlueprintPure, Category = "Ballistics|Configuration|Mesh") + FTransform GetMuzzleTransform() const; + + UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Mesh") + void SetMuzzleSocketName(const FString& NewSocketName); + + // Ballistics Calculations + UFUNCTION(BlueprintPure, Category = "Ballistics|Calculations|Properties") float GetBarrelLength() const; - UFUNCTION(BlueprintPure, Category = "Gun|Barrel") + UFUNCTION(BlueprintPure, Category = "Ballistics|Calculations|Properties") float GetEffectiveMuzzleVelocity() const; - UFUNCTION(BlueprintPure, Category = "Gun|Barrel") + UFUNCTION(BlueprintPure, Category = "Ballistics|Calculations|Properties") float GetBarrelAccuracy() const; - UFUNCTION(BlueprintCallable, Category = "Gun|Barrel") + + + UFUNCTION(BlueprintCallable, Category = "Ballistics|Calculations|Recoil") FVector CalculateRecoilImpulse() const; - UFUNCTION(BlueprintPure, Category = "Gun|Barrel") + // Legacy barrel support + UFUNCTION(BlueprintPure, Category = "Ballistics|Legacy|Barrel") bool IsBarrelConnected() const; - // Events + // ======================================== + // BLUEPRINT EVENTS + // ======================================== DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGunStateChanged, EGunState, NewState); - UPROPERTY(BlueprintAssignable, Category = "Gun Events") + UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|State") FOnGunStateChanged OnGunStateChanged; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGunFired, UEBBulletPropertiesAsset*, BulletType); - UPROPERTY(BlueprintAssignable, Category = "Gun Events") + UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Firing") FOnGunFired OnGunFired; DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMagazineEmpty); - UPROPERTY(BlueprintAssignable, Category = "Gun Events") + UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Ammunition") FOnMagazineEmpty OnMagazineEmpty; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMagazineChanged, UEBMagazine*, NewMagazine); - UPROPERTY(BlueprintAssignable, Category = "Gun Events") + UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Ammunition") FOnMagazineChanged OnMagazineChanged; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSafetyChanged, bool, bNewSafetyState); - UPROPERTY(BlueprintAssignable, Category = "Gun Events") + UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Safety") FOnSafetyChanged OnSafetyChanged; // Networking virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + // Advanced Firing Functions + UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Advanced") + void Shoot(bool bTrigger); + + UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Gatling") + void GatlingSpool(bool bSpool); + + UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Fire Control") + void SwitchFireMode(EFireMode NewFireMode); + + // ======================================== + // BALLISTICS PREDICTION & CALCULATIONS + // ======================================== + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Ballistics|Prediction|Trajectory") + void PredictHit(bool& bHit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, + AActor*& HitActor, TArray& Trajectory, + TSubclassOf BulletClass, + const TArray& IgnoredActors, + float MaxTime = 10.0f, float Step = 0.1f) const; + + UFUNCTION(BlueprintCallable, Category = "Ballistics|Prediction|Targeting") + void CalculateAimDirection(TSubclassOf BulletClass, FVector TargetLocation, FVector TargetVelocity, + FVector& AimDirection, FVector& PredictedTargetLocation, FVector& PredictedIntersectionLocation, + float& PredictedFlightTime, float& Error, float MaxTime = 10.0f, float Step = 0.1f, int32 NumIterations = 4) const; + protected: virtual void BeginPlay() override; virtual void Tick(float DeltaTime) override; +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + // Replication UFUNCTION() void OnRep_GunState(); @@ -193,12 +421,12 @@ protected: // Firing Control bool bTriggerPressed = false; - bool bCanProcessNextShot = true; + int32 BurstShotsFired = 0; - // Chambered Round - UPROPERTY(Replicated) - UEBBulletPropertiesAsset* ChamberedBulletType; + + // Random Stream for firing calculations + FRandomStream RandomStream; private: EGunState PreviousGunState; @@ -225,10 +453,37 @@ private: UFUNCTION(Server, Reliable) void ServerSetFireMode(EFireMode NewFireMode); + UFUNCTION(Server, Reliable, WithValidation) + void ServerShoot(bool bTrigger); + + UFUNCTION(Server, Reliable, WithValidation) + void ServerGatlingSpool(bool bSpool); + + UFUNCTION(Server, Reliable, WithValidation) + void ServerSwitchFireMode(EFireMode NewFireMode); + + // Server RPC Implementation Declarations + void ServerPullTrigger_Implementation(); + void ServerReleaseTrigger_Implementation(); + void ServerSetSafety_Implementation(bool bNewSafetyState); + void ServerEjectMagazine_Implementation(); + void ServerInsertMagazine_Implementation(UEBMagazine* NewMagazine); + void ServerChargingHandle_Implementation(); + void ServerSetFireMode_Implementation(EFireMode NewFireMode); + void ServerShoot_Implementation(bool bTrigger); + bool ServerShoot_Validate(bool bTrigger); + void ServerGatlingSpool_Implementation(bool bSpool); + bool ServerGatlingSpool_Validate(bool bSpool); + void ServerSwitchFireMode_Implementation(EFireMode NewFireMode); + bool ServerSwitchFireMode_Validate(EFireMode NewFireMode); + // Multicast Functions UFUNCTION(NetMulticast, Reliable) void MulticastOnGunFired(UEBBulletPropertiesAsset* BulletType); + UFUNCTION(NetMulticast, Reliable) + void MulticastShotFired(); + UFUNCTION(NetMulticast, Reliable) void MulticastOnMagazineChanged(UEBMagazine* NewMagazine); diff --git a/Source/EasyBallistics/Public/EBPlayerWeaponController.h b/Source/EasyBallistics/Public/EBPlayerWeaponController.h new file mode 100644 index 0000000..2e3a31c --- /dev/null +++ b/Source/EasyBallistics/Public/EBPlayerWeaponController.h @@ -0,0 +1,429 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/SceneComponent.h" +#include "Engine/EngineTypes.h" +#include "EBGun.h" +#include "EBWeaponData.h" +#include "EBBulletProperties.h" +#include "NiagaraComponent.h" +#include "NiagaraFunctionLibrary.h" +#include "EBPlayerWeaponController.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponChanged, UEBWeaponData*, NewWeapon); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponFired, UEBWeaponData*, WeaponData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponReloaded, UEBWeaponData*, WeaponData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponEmpty, UEBWeaponData*, WeaponData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAmmoChanged, int32, CurrentAmmo, int32, ReserveAmmo); + +/** + * Player Weapon Controller - AAA Game Industry Standard Scene Component + * + * This follows the pattern used in games like Call of Duty: + * - Single persistent weapon actor (never destroyed) + * - Weapon data assets for all variations + * - Hot-swap meshes, stats, and behavior + * - No actor spawning/despawning + * - Inventory is just array of weapon data + * + * Benefits: + * - Zero GC pressure from spawning/destroying + * - Instant weapon switching (just data swap) + * - Memory efficient + * - Network friendly + * - Scales to hundreds of weapons + * - Direct positioning control as scene component + * - Gun spawns directly on this component's transform + */ +UCLASS(Blueprintable, BlueprintType, ClassGroup=(EasyBallistics), meta=(BlueprintSpawnableComponent, DisplayName = "EB Player Weapon Controller", ToolTip = "Player weapon controller scene component for managing weapon inventory and interactions with direct positioning")) +class EASYBALLISTICS_API UEBPlayerWeaponController : public USceneComponent +{ + GENERATED_BODY() + +public: + UEBPlayerWeaponController(); + + // ======================================== + // WEAPON SYSTEM + // ======================================== + + /** Single persistent weapon actor */ + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_WeaponActor, Category = "Weapon System") + AEBGun* WeaponActor; + + /** Current weapon data */ + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_CurrentWeaponData, Category = "Weapon System") + UEBWeaponData* CurrentWeaponData; + + /** Player's weapon inventory (just data assets) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_WeaponInventory, Category = "Weapon System", meta = (AllowedClasses = "EBWeaponData", DisplayName = "Weapon Inventory", ToolTip = "Array of weapon data assets available to the player")) + TArray WeaponInventory; + + /** Current weapon index in inventory */ + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_CurrentWeaponIndex, Category = "Weapon System") + int32 CurrentWeaponIndex = 0; + + /** Default weapon to spawn with */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon System", meta = (AllowedClasses = "EBWeaponData", DisplayName = "Default Weapon", ToolTip = "The default weapon data asset to equip on spawn")) + UEBWeaponData* DefaultWeapon; + + /** Auto-equip first weapon on begin play */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon System") + bool bAutoEquipOnBeginPlay = true; + + // ======================================== + // AMMO SYSTEM + // ======================================== + + /** Current ammo in magazine */ + UPROPERTY(BlueprintReadOnly, Replicated, Category = "Ammo System") + int32 CurrentAmmo = 0; + + /** Reserve ammo */ + UPROPERTY(BlueprintReadOnly, Replicated, Category = "Ammo System") + int32 ReserveAmmo = 0; + + /** Infinite ammo cheat */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ammo System") + bool bInfiniteAmmo = false; + + // ======================================== + // WEAPON SPAWN SETTINGS + // ======================================== + + /** Spawn transform offset relative to this component */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Spawn", meta = (DisplayName = "Weapon Spawn Offset", ToolTip = "Transform offset for spawning the weapon relative to this component")) + FTransform WeaponSpawnOffset = FTransform::Identity; + + // ======================================== + // NIAGARA VFX SYSTEM + // ======================================== + + /** Persistent Niagara components for weapon effects */ + UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX") + UNiagaraComponent* MuzzleFlashComponent; + + UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX") + UNiagaraComponent* MuzzleSmokeComponent; + + UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX") + UNiagaraComponent* BarrelHeatComponent; + + UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX") + UNiagaraComponent* AttachmentGlowComponent; + + /** Current weapon heat level (0-1) */ + UPROPERTY(BlueprintReadOnly, Replicated, Category = "Niagara VFX") + float CurrentHeatLevel = 0.0f; + + /** Enable advanced VFX features */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Niagara VFX") + bool bEnableAdvancedVFX = true; + + // ======================================== + // INPUT ACTIONS + // ======================================== + + /** Primary fire */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void Fire(); + + /** Stop firing */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void StopFire(); + + /** Reload weapon */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void Reload(); + + /** Switch to next weapon */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void NextWeapon(); + + /** Switch to previous weapon */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void PreviousWeapon(); + + /** Switch to specific weapon by index */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void SwitchToWeapon(int32 WeaponIndex); + + /** Switch to specific weapon by data */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void SwitchToWeaponData(UEBWeaponData* WeaponData); + + /** Toggle fire mode */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void ToggleFireMode(); + + /** Toggle safety */ + UFUNCTION(BlueprintCallable, Category = "Weapon Input") + void ToggleSafety(); + + // ======================================== + // WEAPON MANAGEMENT + // ======================================== + + /** Add weapon to inventory */ + UFUNCTION(BlueprintCallable, Category = "Weapon Management") + void AddWeapon(UEBWeaponData* WeaponData); + + /** Remove weapon from inventory */ + UFUNCTION(BlueprintCallable, Category = "Weapon Management") + void RemoveWeapon(UEBWeaponData* WeaponData); + + /** Clear all weapons */ + UFUNCTION(BlueprintCallable, Category = "Weapon Management") + void ClearWeapons(); + + /** Set entire weapon inventory */ + UFUNCTION(BlueprintCallable, Category = "Weapon Management") + void SetWeaponInventory(const TArray& NewInventory); + + /** Apply weapon data to actor */ + UFUNCTION(BlueprintCallable, Category = "Weapon Management") + void ApplyWeaponData(UEBWeaponData* WeaponData); + + // ======================================== + // AMMO MANAGEMENT + // ======================================== + + /** Add ammo to reserve */ + UFUNCTION(BlueprintCallable, Category = "Ammo Management") + void AddAmmo(int32 Amount); + + /** Set current ammo */ + UFUNCTION(BlueprintCallable, Category = "Ammo Management") + void SetCurrentAmmo(int32 Amount); + + /** Set reserve ammo */ + UFUNCTION(BlueprintCallable, Category = "Ammo Management") + void SetReserveAmmo(int32 Amount); + + /** Refill all ammo */ + UFUNCTION(BlueprintCallable, Category = "Ammo Management") + void RefillAmmo(); + + // ======================================== + // STATUS QUERIES + // ======================================== + + /** Can weapon fire */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + bool CanFire() const; + + /** Is weapon equipped */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + bool HasWeapon() const; + + /** Get current weapon data */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + UEBWeaponData* GetCurrentWeaponData() const { return CurrentWeaponData; } + + /** Get weapon count */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + int32 GetWeaponCount() const { return WeaponInventory.Num(); } + + /** Get weapon at index */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + UEBWeaponData* GetWeaponAtIndex(int32 Index) const; + + /** Find weapon index */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + int32 FindWeaponIndex(UEBWeaponData* WeaponData) const; + + /** Has weapon in inventory */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + bool HasWeaponInInventory(UEBWeaponData* WeaponData) const; + + /** Get current fire mode */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + EFireMode GetFireMode() const; + + /** Is safety on */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + bool IsSafetyOn() const; + + /** Get muzzle transform */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + FTransform GetMuzzleTransform() const; + + /** Get current heat level */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + float GetHeatLevel() const { return CurrentHeatLevel; } + + /** Is weapon overheated */ + UFUNCTION(BlueprintPure, Category = "Weapon Status") + bool IsOverheated() const; + + // ======================================== + // DEBUG FUNCTIONS + // ======================================== + + /** Print comprehensive weapon debug information to log */ + UFUNCTION(BlueprintCallable, Category = "Debug") + void PrintWeaponDebugInfo() const; + + // ======================================== + // NIAGARA VFX CONTROL + // ======================================== + + /** Trigger muzzle flash effect */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void TriggerMuzzleFlash(); + + /** Trigger shell ejection effect */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void TriggerShellEjection(); + + /** Spawn tracer effect */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void SpawnTracerEffect(const FVector& StartLocation, const FVector& EndLocation); + + /** Spawn impact effect */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void SpawnImpactEffect(const FVector& ImpactLocation, const FVector& ImpactNormal, UPhysicalMaterial* SurfaceMaterial = nullptr); + + /** Update weapon heat effects */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void UpdateHeatEffects(); + + /** Set Niagara parameter on all weapon effects */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void SetNiagaraFloatParameter(const FString& ParameterName, float Value); + + /** Set Niagara vector parameter on all weapon effects */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void SetNiagaraVectorParameter(const FString& ParameterName, const FVector& Value); + + /** Set Niagara color parameter on all weapon effects */ + UFUNCTION(BlueprintCallable, Category = "Niagara VFX") + void SetNiagaraColorParameter(const FString& ParameterName, const FLinearColor& Value); + + // ======================================== + // EVENTS + // ======================================== + + /** Called when weapon changes */ + UPROPERTY(BlueprintAssignable, Category = "Weapon Events") + FOnWeaponChanged OnWeaponChanged; + + /** Called when weapon fires */ + UPROPERTY(BlueprintAssignable, Category = "Weapon Events") + FOnWeaponFired OnWeaponFired; + + /** Called when weapon reloads */ + UPROPERTY(BlueprintAssignable, Category = "Weapon Events") + FOnWeaponReloaded OnWeaponReloaded; + + /** Called when weapon is empty */ + UPROPERTY(BlueprintAssignable, Category = "Weapon Events") + FOnWeaponEmpty OnWeaponEmpty; + + /** Called when ammo changes */ + UPROPERTY(BlueprintAssignable, Category = "Weapon Events") + FOnAmmoChanged OnAmmoChanged; + + /** Called when WeaponActor is replicated */ + UFUNCTION() + void OnRep_WeaponActor(); + + // ======================================== + // OVERRIDES + // ======================================== + + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + +protected: + // ======================================== + // INTERNAL FUNCTIONS + // ======================================== + + /** Create weapon actor */ + void CreateWeaponActor(); + + /** Apply weapon data to gun actor */ + void InternalApplyWeaponData(UEBWeaponData* WeaponData); + + /** Update ammo from weapon data */ + void UpdateAmmoFromWeaponData(); + + /** Handle weapon fired */ + UFUNCTION() + void HandleWeaponFired(UEBBulletPropertiesAsset* BulletType); + + /** Handle magazine empty */ + UFUNCTION() + void HandleMagazineEmpty(); + + /** Bind weapon events */ + void BindWeaponEvents(); + + /** Unbind weapon events */ + void UnbindWeaponEvents(); + + /** Check authority */ + bool HasAuthority() const; + + /** Create Niagara components */ + void CreateNiagaraComponents(); + + /** Update Niagara components with weapon data */ + void UpdateNiagaraComponents(); + + /** Apply Niagara parameters from weapon data */ + void ApplyNiagaraParameters(UNiagaraComponent* Component, const FNiagaraEffectParams& Params); + + /** Update heat level and effects */ + void UpdateWeaponHeat(float DeltaTime); + +private: + // ======================================== + // SERVER RPCS + // ======================================== + + UFUNCTION(Server, Reliable) + void ServerSwitchToWeapon(int32 WeaponIndex); + + UFUNCTION(Server, Reliable) + void ServerSwitchToWeaponData(UEBWeaponData* WeaponData); + + UFUNCTION(Server, Reliable) + void ServerAddWeapon(UEBWeaponData* WeaponData); + + UFUNCTION(Server, Reliable) + void ServerRemoveWeapon(UEBWeaponData* WeaponData); + + UFUNCTION(Server, Reliable) + void ServerSetCurrentAmmo(int32 Amount); + + UFUNCTION(Server, Reliable) + void ServerSetReserveAmmo(int32 Amount); + + UFUNCTION(Server, Reliable) + void ServerToggleFireMode(); + + UFUNCTION(Server, Reliable) + void ServerToggleSafety(); + + // ======================================== + // REPLICATION + // ======================================== + + UFUNCTION() + void OnRep_CurrentWeaponData(); + + UFUNCTION() + void OnRep_WeaponInventory(); + + UFUNCTION() + void OnRep_CurrentWeaponIndex(); +}; \ No newline at end of file diff --git a/Source/EasyBallistics/Public/EBWeaponData.h b/Source/EasyBallistics/Public/EBWeaponData.h new file mode 100644 index 0000000..94b6270 --- /dev/null +++ b/Source/EasyBallistics/Public/EBWeaponData.h @@ -0,0 +1,619 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "Engine/SkeletalMesh.h" +#include "Engine/StaticMesh.h" +#include "Animation/AnimSequence.h" +#include "Animation/AnimBlueprint.h" +#include "Sound/SoundCue.h" +#include "NiagaraSystem.h" +#include "NiagaraComponent.h" +#include "NiagaraFunctionLibrary.h" +#include "NiagaraDataInterface.h" +#include "EBBulletProperties.h" +#include "EBBarrel.h" +#include "EBWeaponData.generated.h" + +/** + * Weapon Type Classification + */ +UENUM(BlueprintType) +enum class EWeaponType : uint8 +{ + Rifle UMETA(DisplayName = "Rifle"), + Pistol UMETA(DisplayName = "Pistol"), + Shotgun UMETA(DisplayName = "Shotgun"), + SMG UMETA(DisplayName = "SMG"), + LMG UMETA(DisplayName = "LMG"), + Sniper UMETA(DisplayName = "Sniper"), + Launcher UMETA(DisplayName = "Launcher"), + Melee UMETA(DisplayName = "Melee"), + Special UMETA(DisplayName = "Special") +}; + +/** + * Weapon Audio Configuration + */ +USTRUCT(BlueprintType) +struct FWeaponAudio +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio") + USoundCue* FireSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio") + USoundCue* EmptyFireSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio") + USoundCue* ReloadSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio") + USoundCue* EquipSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio") + USoundCue* UnequipSound; + + FWeaponAudio() + { + FireSound = nullptr; + EmptyFireSound = nullptr; + ReloadSound = nullptr; + EquipSound = nullptr; + UnequipSound = nullptr; + } +}; + +/** + * Niagara Effect Parameters for Dynamic Control + */ +USTRUCT(BlueprintType) +struct FNiagaraEffectParams +{ + GENERATED_USTRUCT_BODY() + + /** Effect intensity multiplier */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters", meta = (ClampMin = "0.0", ClampMax = "5.0")) + float Intensity = 1.0f; + + /** Effect scale multiplier */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters", meta = (ClampMin = "0.1", ClampMax = "10.0")) + float Scale = 1.0f; + + /** Effect color tint */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters") + FLinearColor ColorTint = FLinearColor::White; + + /** Custom float parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters") + TMap FloatParameters; + + /** Custom vector parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters") + TMap VectorParameters; + + /** Custom boolean parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters") + TMap BoolParameters; +}; + +/** + * Advanced Niagara Weapon Visual Effects Configuration + */ +USTRUCT(BlueprintType) +struct FWeaponVFX +{ + GENERATED_USTRUCT_BODY() + + // === MUZZLE EFFECTS === + /** Primary muzzle flash effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle") + UNiagaraSystem* MuzzleFlash; + + /** Muzzle flash parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle") + FNiagaraEffectParams MuzzleFlashParams; + + /** Muzzle smoke effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle") + UNiagaraSystem* MuzzleSmoke; + + /** Muzzle smoke parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle") + FNiagaraEffectParams MuzzleSmokeParams; + + /** Suppressed muzzle effect (when suppressor attached) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle") + UNiagaraSystem* SuppressedMuzzleFlash; + + // === SHELL EJECTION === + /** Ejected shell/casing effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells") + UNiagaraSystem* EjectedShell; + + /** Shell ejection parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells") + FNiagaraEffectParams EjectedShellParams; + + /** Shell ejection velocity */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells") + FVector ShellEjectionVelocity = FVector(200.0f, 100.0f, 50.0f); + + /** Shell physics material for bouncing sounds */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells") + UPhysicalMaterial* ShellPhysicsMaterial; + + // === PROJECTILE EFFECTS === + /** Bullet tracer effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile") + UNiagaraSystem* TracerEffect; + + /** Tracer parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile") + FNiagaraEffectParams TracerParams; + + /** Bullet trail/wake effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile") + UNiagaraSystem* BulletTrail; + + /** Supersonic crack effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile") + UNiagaraSystem* SonicCrack; + + // === IMPACT EFFECTS === + /** Default hit impact effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact") + UNiagaraSystem* HitImpact; + + /** Impact parameters */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact") + FNiagaraEffectParams HitImpactParams; + + /** Material-specific impact effects */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact") + TMap MaterialImpactEffects; + + /** Penetration effect (when bullet goes through) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact") + UNiagaraSystem* PenetrationEffect; + + /** Ricochet effect */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact") + UNiagaraSystem* RicochetEffect; + + // === ADVANCED SETTINGS === + /** Enable dynamic weather effects (rain interaction, etc.) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced") + bool bEnableWeatherEffects = true; + + /** Enable heat distortion from barrel */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced") + bool bEnableHeatDistortion = true; + + /** Heat buildup effect from sustained fire */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced") + UNiagaraSystem* BarrelHeatEffect; + + /** Weapon attachment glow/energy effects */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced") + UNiagaraSystem* AttachmentGlowEffect; + + /** LOD settings for performance */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced") + float EffectLODDistance = 1000.0f; + + FWeaponVFX() + { + // Initialize all effects to null + MuzzleFlash = nullptr; + MuzzleSmoke = nullptr; + SuppressedMuzzleFlash = nullptr; + EjectedShell = nullptr; + TracerEffect = nullptr; + BulletTrail = nullptr; + SonicCrack = nullptr; + HitImpact = nullptr; + PenetrationEffect = nullptr; + RicochetEffect = nullptr; + BarrelHeatEffect = nullptr; + AttachmentGlowEffect = nullptr; + ShellPhysicsMaterial = nullptr; + ShellEjectionVelocity = FVector(200.0f, 100.0f, 50.0f); + bEnableWeatherEffects = true; + bEnableHeatDistortion = true; + EffectLODDistance = 1000.0f; + } +}; + +/** + * Weapon Animation Configuration + */ +USTRUCT(BlueprintType) +struct FWeaponAnimations +{ + GENERATED_USTRUCT_BODY() + + /** First person weapon animations */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP") + UAnimSequence* FP_Fire; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP") + UAnimSequence* FP_Reload; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP") + UAnimSequence* FP_Equip; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP") + UAnimSequence* FP_Idle; + + /** Third person character animations */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP") + UAnimSequence* TP_Fire; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP") + UAnimSequence* TP_Reload; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP") + UAnimSequence* TP_Equip; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP") + UAnimSequence* TP_Idle; + + /** Animation blueprint for complex animations */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations") + TSubclassOf WeaponAnimBP; + + FWeaponAnimations() + { + FP_Fire = nullptr; + FP_Reload = nullptr; + FP_Equip = nullptr; + FP_Idle = nullptr; + TP_Fire = nullptr; + TP_Reload = nullptr; + TP_Equip = nullptr; + TP_Idle = nullptr; + WeaponAnimBP = nullptr; + } +}; + +/** + * Weapon Data Asset - Industry Standard Approach + * + * This is how AAA games handle weapons: + * - Single persistent weapon actor + * - All weapon variations as data assets + * - Hot-swap meshes, stats, behavior + * - No actor spawning/despawning + * - Inventory is just array of data references + */ +UCLASS(BlueprintType, Blueprintable) +class EASYBALLISTICS_API UEBWeaponData : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + UEBWeaponData(); + + // ======================================== + // WEAPON IDENTITY + // ======================================== + + /** Weapon display name */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity") + FText WeaponName; + + /** Weapon description */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity") + FText WeaponDescription; + + /** Weapon type classification */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity") + EWeaponType WeaponType; + + /** Weapon icon for UI */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity") + UTexture2D* WeaponIcon; + + /** Weapon rarity/tier */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity") + int32 WeaponTier = 1; + + // ======================================== + // VISUAL COMPONENTS + // ======================================== + + /** First person weapon mesh */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals") + USkeletalMesh* FirstPersonMesh; + + /** Third person weapon mesh */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals") + USkeletalMesh* ThirdPersonMesh; + + /** Static mesh fallback */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals") + UStaticMesh* StaticMesh; + + /** Material overrides for weapon customization */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals") + TArray MaterialOverrides; + + // ======================================== + // BALLISTICS & PERFORMANCE + // ======================================== + + /** Bullet properties for this weapon */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics") + UEBBulletPropertiesAsset* BulletProperties; + + /** Damage per shot */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "1", ClampMax = "1000")) + float Damage = 30.0f; + + /** Fire rate (rounds per minute) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "1", ClampMax = "1200")) + float FireRate = 600.0f; + + /** Muzzle velocity in cm/s */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "10000", ClampMax = "200000")) + float MuzzleVelocity = 91440.0f; + + /** Weapon accuracy (0-1, 1 = perfect) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "0.0", ClampMax = "1.0")) + float Accuracy = 0.95f; + + /** Recoil strength */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "0.0", ClampMax = "10.0")) + float RecoilStrength = 1.0f; + + /** Range in meters */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "10", ClampMax = "2000")) + float EffectiveRange = 300.0f; + + // ======================================== + // MAGAZINE & AMMO + // ======================================== + + /** Magazine capacity */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "1", ClampMax = "200")) + int32 MagazineSize = 30; + + /** Reserve ammo capacity */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "0", ClampMax = "1000")) + int32 MaxReserveAmmo = 120; + + /** Reload time in seconds */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "0.5", ClampMax = "10.0")) + float ReloadTime = 2.5f; + + /** Time for tactical reload (magazine not empty) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "0.5", ClampMax = "10.0")) + float TacticalReloadTime = 2.0f; + + // ======================================== + // FIRE MODES + // ======================================== + + /** Available fire modes */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fire Modes") + TArray AvailableFireModes; + + /** Default fire mode */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fire Modes") + EFireMode DefaultFireMode = EFireMode::FM_Auto; + + /** Burst count for burst fire */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fire Modes", meta = (ClampMin = "2", ClampMax = "10")) + int32 BurstCount = 3; + + // ======================================== + // ATTACHMENTS & CUSTOMIZATION + // ======================================== + + /** Socket names for attachments */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments") + FName MuzzleSocketName = TEXT("MuzzleSocket"); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments") + FName ScopeSocketName = TEXT("ScopeSocket"); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments") + FName GripSocketName = TEXT("GripSocket"); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments") + FName LaserSocketName = TEXT("LaserSocket"); + + /** Supported attachment types */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments") + TArray SupportedAttachments; + + // ======================================== + // AUDIO & VFX + // ======================================== + + /** Audio configuration */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Audio & VFX") + FWeaponAudio AudioConfig; + + /** Niagara visual effects configuration */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Audio & VFX") + FWeaponVFX VFXConfig; + + // ======================================== + // NIAGARA ADVANCED FEATURES + // ======================================== + + /** Global Niagara parameters for this weapon */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced") + FNiagaraEffectParams GlobalEffectParams; + + /** Enable GPU simulation for high particle counts */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced") + bool bUseGPUSimulation = true; + + /** Enable distance-based LOD for effects */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced") + bool bEnableEffectLOD = true; + + /** Custom data interface for weapon stats */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced") + TSubclassOf WeaponDataInterface; + + /** Heat tracking for barrel effects */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced") + float MaxHeatLevel = 100.0f; + + /** Heat decay rate per second */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced") + float HeatDecayRate = 10.0f; + + /** Heat gain per shot */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced") + float HeatPerShot = 5.0f; + + /** Animation configuration */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Audio & VFX") + FWeaponAnimations AnimationConfig; + + // ======================================== + // ADVANCED SETTINGS + // ======================================== + + /** Weapon weight (affects movement speed) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.5", ClampMax = "20.0")) + float WeaponWeight = 3.5f; + + /** ADS (Aim Down Sights) time */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.1", ClampMax = "2.0")) + float ADSTime = 0.3f; + + /** Sprint-to-fire time */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.1", ClampMax = "1.0")) + float SprintToFireTime = 0.25f; + + /** Draw/Equip time */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.2", ClampMax = "3.0")) + float EquipTime = 0.75f; + + /** Whether weapon can be dual-wielded */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced") + bool bCanDualWield = false; + + /** Whether weapon has safety */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced") + bool bHasSafety = true; + + /** Whether weapon supports suppressors */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced") + bool bSuppressorCompatible = true; + + // ======================================== + // DEBUG SETTINGS + // ======================================== + + /** Enable debug logging for weapon firing */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") + bool bEnableFireDebug = false; + + /** Size of debug arrows for ballistic visualization */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug", meta = (ClampMin = "10.0", ClampMax = "500.0")) + float DebugArrowSize = 100.0f; + + /** Show impact information in debug display */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") + bool bDebugImpactInfo = false; + + /** Enable trajectory debug visualization */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") + bool bDebugTrajectory = false; + + /** Enable physics debug visualization (drag, gravity, wind) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") + bool bDebugPhysics = false; + + /** Enable performance debug information */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") + bool bDebugPerformance = false; + + /** Enable ballistics debug visualization */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") + bool bDebugBallistics = false; + + /** Enable spalling debug visualization */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") + bool bDebugSpalling = false; + + // ======================================== + // UTILITY FUNCTIONS + // ======================================== + + /** Get fire rate in shots per second */ + UFUNCTION(BlueprintPure, Category = "Weapon Data") + float GetFireRatePerSecond() const { return FireRate / 60.0f; } + + /** Get time between shots */ + UFUNCTION(BlueprintPure, Category = "Weapon Data") + float GetTimeBetweenShots() const { return 60.0f / FireRate; } + + /** Get DPS (Damage Per Second) */ + UFUNCTION(BlueprintPure, Category = "Weapon Data") + float GetDPS() const { return Damage * GetFireRatePerSecond(); } + + /** Check if fire mode is supported */ + UFUNCTION(BlueprintPure, Category = "Weapon Data") + bool IsFireModeSupported(EFireMode FireMode) const { return AvailableFireModes.Contains(FireMode); } + + /** Get weapon type as string */ + UFUNCTION(BlueprintPure, Category = "Weapon Data") + FString GetWeaponTypeString() const; + + /** Get current heat level (0-1) */ + UFUNCTION(BlueprintPure, Category = "Weapon Data") + float GetHeatLevel() const { return CurrentHeat / MaxHeatLevel; } + + /** Add heat from firing */ + UFUNCTION(BlueprintCallable, Category = "Weapon Data") + void AddHeat(float Amount = -1.0f); + + /** Update heat decay */ + UFUNCTION(BlueprintCallable, Category = "Weapon Data") + void UpdateHeat(float DeltaTime); + + /** Is weapon overheated */ + UFUNCTION(BlueprintPure, Category = "Weapon Data") + bool IsOverheated() const { return CurrentHeat >= MaxHeatLevel; } + + // ======================================== + // DEBUG UTILITY FUNCTIONS + // ======================================== + + /** Enable/disable specific bullet debug category */ + UFUNCTION(BlueprintCallable, Category = "Debug") + void SetBulletDebugCategory(const FString& CategoryName, bool bEnabled); + + /** Enable/disable all bullet debug categories */ + UFUNCTION(BlueprintCallable, Category = "Debug") + void SetAllBulletDebugCategories(bool bEnabled); + + /** Get current debug configuration as string */ + UFUNCTION(BlueprintPure, Category = "Debug") + FString GetBulletDebugInfo() const; + + /** Apply debug settings to connected barrel component */ + UFUNCTION(BlueprintCallable, Category = "Debug") + void ApplyDebugSettingsToBarrel(class UEBBarrel* Barrel) const; + +private: + /** Current heat level for effects */ + float CurrentHeat = 0.0f; + + /** Get primary data asset ID */ + virtual FPrimaryAssetId GetPrimaryAssetId() const override + { + return FPrimaryAssetId("WeaponData", GetFName()); + } +}; \ No newline at end of file diff --git a/Source/EasyBallisticsEditor/EasyBallisticsEditor.Build.cs b/Source/EasyBallisticsEditor/EasyBallisticsEditor.Build.cs index 20fc97d..824be38 100644 --- a/Source/EasyBallisticsEditor/EasyBallisticsEditor.Build.cs +++ b/Source/EasyBallisticsEditor/EasyBallisticsEditor.Build.cs @@ -21,12 +21,13 @@ public class EasyBallisticsEditor : ModuleRules "EditorStyle", "EditorSubsystem", "LevelEditor", - "SceneOutliner" + "SceneOutliner", + "InputCore", + "Slate", + "SlateCore" }); PrivateDependencyModuleNames.AddRange(new string[] { - "Slate", - "SlateCore", "PropertyEditor", "WorkspaceMenuStructure", "DesktopPlatform", diff --git a/Source/EasyBallisticsEditor/Private/EBBarrelComponentFactory.cpp b/Source/EasyBallisticsEditor/Private/EBBarrelComponentFactory.cpp index 045f8f7..98e87db 100644 --- a/Source/EasyBallisticsEditor/Private/EBBarrelComponentFactory.cpp +++ b/Source/EasyBallisticsEditor/Private/EBBarrelComponentFactory.cpp @@ -2,6 +2,7 @@ #include "EBBarrelComponentFactory.h" #include "EBBarrel.h" +#include "EBGun.h" #include "Engine/Selection.h" #include "Components/SceneComponent.h" #include "DrawDebugHelpers.h" @@ -25,15 +26,21 @@ void FEBBarrelComponentVisualizer::DrawVisualization(const UActorComponent* Comp // Draw barrel representation FColor BarrelColor = FColor::Blue; - if (BarrelComponent->ChamberedBullet) + + const AEBGun* ParentGun = BarrelComponent->GetParentGun(); + if (ParentGun) { - BarrelColor = FColor::Green; // Loaded - } - else if (BarrelComponent->Shooting) - { - BarrelColor = FColor::Red; // Firing + if (ParentGun->bChamberedRound) + { + BarrelColor = FColor::Green; // Loaded + } + else if (ParentGun->CurrentGunState == EGunState::GS_Firing) + { + BarrelColor = FColor::Red; // Firing + } } + // Draw barrel cylinder float BarrelLength = FMath::Max(BarrelComponent->BarrelLength, 10.0f); // Minimum visual length float BarrelRadius = 2.0f; @@ -47,7 +54,7 @@ void FEBBarrelComponentVisualizer::DrawVisualization(const UActorComponent* Comp DrawWireBox(PDI, FBox(MuzzleLocation - FVector(2, 2, 2), MuzzleLocation + FVector(2, 2, 2)), FColor::Orange, SDPG_Foreground); // Draw firing direction indicator - if (BarrelComponent->Shooting) + if (ParentGun && ParentGun->CurrentGunState == EGunState::GS_Firing) { FVector FireDirection = BarrelEnd + (Forward * 20.0f); PDI->DrawLine(BarrelEnd, FireDirection, FColor::Red, SDPG_Foreground, 2.0f); @@ -77,10 +84,10 @@ void FEBBarrelComponentVisualizer::DrawVisualization(const UActorComponent* Comp } // Draw ammo count indicator - if (BarrelComponent->Ammo.Num() > 0) + if (ParentGun && ParentGun->GetRoundsInMagazine() > 0) { FVector AmmoIndicatorLocation = Location + (Right * 10.0f); - for (int32 i = 0; i < FMath::Min(BarrelComponent->Ammo.Num(), 10); i++) + for (int32 i = 0; i < FMath::Min(ParentGun->GetRoundsInMagazine(), 10); i++) { FVector AmmoLocation = AmmoIndicatorLocation + (Up * i * 3.0f); FVector BoxExtent = FVector(0.5f, 0.5f, 1.0f); diff --git a/Source/EasyBallisticsEditor/Private/EBPlayerWeaponControllerCustomization.cpp b/Source/EasyBallisticsEditor/Private/EBPlayerWeaponControllerCustomization.cpp new file mode 100644 index 0000000..144339c --- /dev/null +++ b/Source/EasyBallisticsEditor/Private/EBPlayerWeaponControllerCustomization.cpp @@ -0,0 +1,36 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#include "EBPlayerWeaponControllerCustomization.h" +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "IDetailPropertyRow.h" +#include "Widgets/Text/STextBlock.h" +#include "EBPlayerWeaponController.h" + +#define LOCTEXT_NAMESPACE "EBPlayerWeaponControllerCustomization" + +TSharedRef FEBPlayerWeaponControllerCustomization::MakeInstance() +{ + return MakeShareable(new FEBPlayerWeaponControllerCustomization); +} + +void FEBPlayerWeaponControllerCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + // Since the weapon controller is now a scene component, we don't need complex attachment customization + // The component can be positioned directly in the viewport + + // Add a help text to guide users + IDetailCategoryBuilder& WeaponSpawnCategory = DetailBuilder.EditCategory("Weapon Spawn"); + + WeaponSpawnCategory.AddCustomRow(LOCTEXT("PositioningHelp", "Positioning Help")) + .WholeRowContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("PositioningHelpText", "Position this component in the viewport where you want weapons to appear. You can attach it to bones/sockets for animation.")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .AutoWrapText(true) + ]; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/EasyBallisticsEditor/Private/EBWeaponDataFactory.cpp b/Source/EasyBallisticsEditor/Private/EBWeaponDataFactory.cpp new file mode 100644 index 0000000..bc8c806 --- /dev/null +++ b/Source/EasyBallisticsEditor/Private/EBWeaponDataFactory.cpp @@ -0,0 +1,248 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#include "EBWeaponDataFactory.h" +#include "EBWeaponData.h" +#include "EBWeaponConfiguration.h" +#include "AssetToolsModule.h" +#include "EasyBallisticsEditor.h" + +#define LOCTEXT_NAMESPACE "EBWeaponDataFactory" + +UEBWeaponDataFactory::UEBWeaponDataFactory() +{ + bCreateNew = true; + bEditAfterNew = true; + SupportedClass = UEBWeaponData::StaticClass(); +} + +UObject* UEBWeaponDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UEBWeaponData* WeaponData = NewObject(InParent, Class, Name, Flags); + + if (WeaponData) + { + // Set up default configuration based on name + FString AssetName = Name.ToString(); + + if (AssetName.Contains(TEXT("M4")) || AssetName.Contains(TEXT("AR15")) || AssetName.Contains(TEXT("Rifle"))) + { + // Configure for M4/AR-15 type weapon + WeaponData->WeaponName = FText::FromString(TEXT("M4 Carbine")); + WeaponData->WeaponDescription = FText::FromString(TEXT("5.56mm assault rifle with selective fire capabilities")); + WeaponData->WeaponType = EWeaponType::Rifle; + WeaponData->WeaponTier = 3; + WeaponData->Damage = 35.0f; + WeaponData->FireRate = 750.0f; + WeaponData->MuzzleVelocity = 91440.0f; // ~3000 fps + WeaponData->Accuracy = 0.92f; + WeaponData->RecoilStrength = 1.2f; + WeaponData->EffectiveRange = 400.0f; + WeaponData->MagazineSize = 30; + WeaponData->MaxReserveAmmo = 180; + WeaponData->ReloadTime = 2.5f; + WeaponData->TacticalReloadTime = 2.0f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto); + WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto); + WeaponData->DefaultFireMode = EFireMode::FM_Auto; + WeaponData->WeaponWeight = 3.4f; + WeaponData->ADSTime = 0.35f; + WeaponData->EquipTime = 0.8f; + } + else if (AssetName.Contains(TEXT("AK")) || AssetName.Contains(TEXT("74"))) + { + // Configure for AK type weapon + WeaponData->WeaponName = FText::FromString(TEXT("AK-74")); + WeaponData->WeaponDescription = FText::FromString(TEXT("5.45mm assault rifle with proven reliability")); + WeaponData->WeaponType = EWeaponType::Rifle; + WeaponData->WeaponTier = 3; + WeaponData->Damage = 32.0f; + WeaponData->FireRate = 650.0f; + WeaponData->MuzzleVelocity = 88500.0f; // ~2900 fps + WeaponData->Accuracy = 0.88f; + WeaponData->RecoilStrength = 1.4f; + WeaponData->EffectiveRange = 350.0f; + WeaponData->MagazineSize = 30; + WeaponData->MaxReserveAmmo = 180; + WeaponData->ReloadTime = 2.8f; + WeaponData->TacticalReloadTime = 2.2f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto); + WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto); + WeaponData->DefaultFireMode = EFireMode::FM_Auto; + WeaponData->WeaponWeight = 3.6f; + WeaponData->ADSTime = 0.4f; + WeaponData->EquipTime = 0.9f; + } + else if (AssetName.Contains(TEXT("Pistol")) || AssetName.Contains(TEXT("Glock")) || AssetName.Contains(TEXT("1911"))) + { + // Configure for pistol + WeaponData->WeaponName = FText::FromString(TEXT("M1911")); + WeaponData->WeaponDescription = FText::FromString(TEXT(".45 ACP semi-automatic pistol")); + WeaponData->WeaponType = EWeaponType::Pistol; + WeaponData->WeaponTier = 2; + WeaponData->Damage = 55.0f; + WeaponData->FireRate = 350.0f; + WeaponData->MuzzleVelocity = 76200.0f; // ~2500 fps + WeaponData->Accuracy = 0.85f; + WeaponData->RecoilStrength = 2.0f; + WeaponData->EffectiveRange = 80.0f; + WeaponData->MagazineSize = 7; + WeaponData->MaxReserveAmmo = 35; + WeaponData->ReloadTime = 1.8f; + WeaponData->TacticalReloadTime = 1.5f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto); + WeaponData->DefaultFireMode = EFireMode::FM_Semiauto; + WeaponData->WeaponWeight = 1.1f; + WeaponData->ADSTime = 0.2f; + WeaponData->EquipTime = 0.4f; + } + else if (AssetName.Contains(TEXT("Sniper")) || AssetName.Contains(TEXT("Bolt"))) + { + // Configure for bolt-action sniper rifle + WeaponData->WeaponName = FText::FromString(TEXT("M24 SWS")); + WeaponData->WeaponDescription = FText::FromString(TEXT("7.62mm bolt-action sniper rifle")); + WeaponData->WeaponType = EWeaponType::Sniper; + WeaponData->WeaponTier = 4; + WeaponData->Damage = 120.0f; + WeaponData->FireRate = 60.0f; + WeaponData->MuzzleVelocity = 108000.0f; // ~3540 fps + WeaponData->Accuracy = 0.98f; + WeaponData->RecoilStrength = 3.5f; + WeaponData->EffectiveRange = 800.0f; + WeaponData->MagazineSize = 5; + WeaponData->MaxReserveAmmo = 30; + WeaponData->ReloadTime = 3.5f; + WeaponData->TacticalReloadTime = 3.0f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Manual); + WeaponData->DefaultFireMode = EFireMode::FM_Manual; + WeaponData->WeaponWeight = 5.5f; + WeaponData->ADSTime = 0.6f; + WeaponData->EquipTime = 1.2f; + } + else if (AssetName.Contains(TEXT("Shotgun"))) + { + // Configure for shotgun + WeaponData->WeaponName = FText::FromString(TEXT("M870")); + WeaponData->WeaponDescription = FText::FromString(TEXT("12-gauge pump-action shotgun")); + WeaponData->WeaponType = EWeaponType::Shotgun; + WeaponData->WeaponTier = 2; + WeaponData->Damage = 80.0f; + WeaponData->FireRate = 120.0f; + WeaponData->MuzzleVelocity = 38100.0f; // ~1250 fps + WeaponData->Accuracy = 0.6f; + WeaponData->RecoilStrength = 4.0f; + WeaponData->EffectiveRange = 50.0f; + WeaponData->MagazineSize = 8; + WeaponData->MaxReserveAmmo = 40; + WeaponData->ReloadTime = 4.0f; + WeaponData->TacticalReloadTime = 3.5f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Manual); + WeaponData->DefaultFireMode = EFireMode::FM_Manual; + WeaponData->WeaponWeight = 3.2f; + WeaponData->ADSTime = 0.45f; + WeaponData->EquipTime = 0.9f; + } + else if (AssetName.Contains(TEXT("LMG")) || AssetName.Contains(TEXT("Machine"))) + { + // Configure for light machine gun + WeaponData->WeaponName = FText::FromString(TEXT("M249 SAW")); + WeaponData->WeaponDescription = FText::FromString(TEXT("5.56mm light machine gun")); + WeaponData->WeaponType = EWeaponType::LMG; + WeaponData->WeaponTier = 4; + WeaponData->Damage = 28.0f; + WeaponData->FireRate = 850.0f; + WeaponData->MuzzleVelocity = 91440.0f; // ~3000 fps + WeaponData->Accuracy = 0.78f; + WeaponData->RecoilStrength = 2.5f; + WeaponData->EffectiveRange = 600.0f; + WeaponData->MagazineSize = 200; + WeaponData->MaxReserveAmmo = 400; + WeaponData->ReloadTime = 6.0f; + WeaponData->TacticalReloadTime = 5.0f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto); + WeaponData->DefaultFireMode = EFireMode::FM_Auto; + WeaponData->WeaponWeight = 7.5f; + WeaponData->ADSTime = 0.8f; + WeaponData->EquipTime = 1.5f; + } + else if (AssetName.Contains(TEXT("SMG")) || AssetName.Contains(TEXT("Sub"))) + { + // Configure for submachine gun + WeaponData->WeaponName = FText::FromString(TEXT("MP5")); + WeaponData->WeaponDescription = FText::FromString(TEXT("9mm submachine gun")); + WeaponData->WeaponType = EWeaponType::SMG; + WeaponData->WeaponTier = 2; + WeaponData->Damage = 25.0f; + WeaponData->FireRate = 800.0f; + WeaponData->MuzzleVelocity = 76200.0f; // ~2500 fps + WeaponData->Accuracy = 0.82f; + WeaponData->RecoilStrength = 0.8f; + WeaponData->EffectiveRange = 150.0f; + WeaponData->MagazineSize = 30; + WeaponData->MaxReserveAmmo = 120; + WeaponData->ReloadTime = 2.2f; + WeaponData->TacticalReloadTime = 1.8f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto); + WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto); + WeaponData->DefaultFireMode = EFireMode::FM_Auto; + WeaponData->WeaponWeight = 2.5f; + WeaponData->ADSTime = 0.25f; + WeaponData->EquipTime = 0.6f; + } + else + { + // Default generic weapon + WeaponData->WeaponName = FText::FromString(TEXT("Generic Weapon")); + WeaponData->WeaponDescription = FText::FromString(TEXT("A basic weapon configuration")); + WeaponData->WeaponType = EWeaponType::Rifle; + WeaponData->WeaponTier = 1; + WeaponData->Damage = 30.0f; + WeaponData->FireRate = 600.0f; + WeaponData->MuzzleVelocity = 91440.0f; + WeaponData->Accuracy = 0.85f; + WeaponData->RecoilStrength = 1.0f; + WeaponData->EffectiveRange = 300.0f; + WeaponData->MagazineSize = 30; + WeaponData->MaxReserveAmmo = 120; + WeaponData->ReloadTime = 2.5f; + WeaponData->TacticalReloadTime = 2.0f; + WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto); + WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto); + WeaponData->DefaultFireMode = EFireMode::FM_Auto; + WeaponData->WeaponWeight = 3.5f; + WeaponData->ADSTime = 0.3f; + WeaponData->EquipTime = 0.75f; + } + + // Set common default values + WeaponData->MuzzleSocketName = TEXT("MuzzleSocket"); + WeaponData->ScopeSocketName = TEXT("ScopeSocket"); + WeaponData->GripSocketName = TEXT("GripSocket"); + WeaponData->LaserSocketName = TEXT("LaserSocket"); + WeaponData->BurstCount = 3; + WeaponData->SprintToFireTime = 0.25f; + WeaponData->bUseGPUSimulation = true; + WeaponData->bEnableEffectLOD = true; + WeaponData->MaxHeatLevel = 100.0f; + WeaponData->HeatDecayRate = 10.0f; + WeaponData->HeatPerShot = 5.0f; + } + + return WeaponData; +} + +UClass* FEBWeaponDataFactory::GetSupportedClass() const +{ + return UEBWeaponData::StaticClass(); +} + +uint32 FEBWeaponDataFactory::GetCategories() +{ + return FEasyBallisticsEditorModule::GetBallisticsAssetCategory(); +} + +FText FEBWeaponDataFactory::GetAssetDescription(const FAssetData& AssetData) const +{ + return LOCTEXT("EBWeaponDataDescription", "Defines comprehensive weapon properties including visuals, ballistics, audio, VFX, animations, and behavior for game-ready weapons."); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/EasyBallisticsEditor/Private/EasyBallisticsEditor.cpp b/Source/EasyBallisticsEditor/Private/EasyBallisticsEditor.cpp index 7da3335..2df3808 100644 --- a/Source/EasyBallisticsEditor/Private/EasyBallisticsEditor.cpp +++ b/Source/EasyBallisticsEditor/Private/EasyBallisticsEditor.cpp @@ -14,6 +14,7 @@ #include "EBBulletActorFactory.h" #include "EBMathematicalBallisticsFactory.h" #include "EBWeaponConfigurationFactory.h" +#include "EBWeaponDataFactory.h" #include "EBGunActorFactory.h" // Component Visualizers @@ -22,11 +23,13 @@ // Customizations #include "EBPhysicalMaterialCustomization.h" +#include "EBPlayerWeaponControllerCustomization.h" #include "EBJsonImportExportTool.h" // Component Classes #include "EBBarrel.h" #include "EBMagazine.h" +#include "EBPlayerWeaponController.h" #include "PhysicalMaterials/PhysicalMaterial.h" #include "Editor.h" @@ -80,6 +83,13 @@ void FEasyBallisticsEditorModule::StartupModule() RegisteredAssetTypeActions.Add(WeaponConfigurationActions); } + // Register Weapon Data Asset factory + { + TSharedRef WeaponDataActions = MakeShareable(new FEBWeaponDataFactory()); + AssetTools.RegisterAssetTypeActions(WeaponDataActions); + RegisteredAssetTypeActions.Add(WeaponDataActions); + } + // Register UFactory objects (these handle the "Add" menu in Content Browser) // These need to be manually registered to appear in the Add menu @@ -112,6 +122,12 @@ void FEasyBallisticsEditorModule::StartupModule() { NewObject(); } + + // Register Weapon Data UFactory + if (!GetDefault()) + { + NewObject(); + } // Register UObject-based factories for actors if (GEditor) @@ -149,6 +165,12 @@ void FEasyBallisticsEditorModule::StartupModule() UPhysicalMaterial::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FEBPhysicalMaterialCustomization::MakeInstance) ); + + // Register Player Weapon Controller customization + PropertyModule.RegisterCustomClassLayout( + UEBPlayerWeaponController::StaticClass()->GetFName(), + FOnGetDetailCustomizationInstance::CreateStatic(&FEBPlayerWeaponControllerCustomization::MakeInstance) + ); // Register JSON Import/Export tool - delay this until after editor is fully loaded FCoreDelegates::OnFEngineLoopInitComplete.AddLambda([]() @@ -172,11 +194,12 @@ void FEasyBallisticsEditorModule::ShutdownModule() } RegisteredComponentClassNames.Empty(); - // Unregister Physical Material customization + // Unregister customizations if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) { FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); PropertyModule.UnregisterCustomClassLayout(UPhysicalMaterial::StaticClass()->GetFName()); + PropertyModule.UnregisterCustomClassLayout(UEBPlayerWeaponController::StaticClass()->GetFName()); } // Unregister asset type actions diff --git a/Source/EasyBallisticsEditor/Public/EBPlayerWeaponControllerCustomization.h b/Source/EasyBallisticsEditor/Public/EBPlayerWeaponControllerCustomization.h new file mode 100644 index 0000000..a959988 --- /dev/null +++ b/Source/EasyBallisticsEditor/Public/EBPlayerWeaponControllerCustomization.h @@ -0,0 +1,19 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "IDetailCustomization.h" + +/** + * Customization for the EBPlayerWeaponController details panel + * Now simplified since the controller is a scene component with direct positioning + */ +class FEBPlayerWeaponControllerCustomization : public IDetailCustomization +{ +public: + static TSharedRef MakeInstance(); + + // IDetailCustomization interface + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; +}; \ No newline at end of file diff --git a/Source/EasyBallisticsEditor/Public/EBWeaponDataFactory.h b/Source/EasyBallisticsEditor/Public/EBWeaponDataFactory.h new file mode 100644 index 0000000..df6911b --- /dev/null +++ b/Source/EasyBallisticsEditor/Public/EBWeaponDataFactory.h @@ -0,0 +1,34 @@ +// Copyright 2016 Mookie. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AssetTypeActions_Base.h" +#include "Factories/Factory.h" +#include "EBWeaponData.h" +#include "EBWeaponDataFactory.generated.h" + +UCLASS() +class EASYBALLISTICSEDITOR_API UEBWeaponDataFactory : public UFactory +{ + GENERATED_BODY() + +public: + UEBWeaponDataFactory(); + + // UFactory interface + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override { return true; } + virtual FText GetDisplayName() const override { return NSLOCTEXT("EBFactory", "WeaponDataText", "Weapon Data"); } + virtual FText GetToolTip() const override { return NSLOCTEXT("EBFactory", "WeaponDataTooltip", "Creates a new Weapon Data asset defining weapon properties, visuals, ballistics, and behavior"); } +}; + +class FEBWeaponDataFactory : public FAssetTypeActions_Base +{ +public: + virtual FText GetName() const override { return NSLOCTEXT("AssetTypeActions", "EBWeaponData", "Weapon Data"); } + virtual FColor GetTypeColor() const override { return FColor(100, 255, 100); } + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + virtual FText GetAssetDescription(const FAssetData& AssetData) const override; +}; \ No newline at end of file diff --git a/docs/docs/advanced/researcher-guide.md b/docs/docs/advanced/researcher-guide.md new file mode 100644 index 0000000..a4f61f3 --- /dev/null +++ b/docs/docs/advanced/researcher-guide.md @@ -0,0 +1,424 @@ +# EasyBallistics Researcher Guide + +## Overview + +This guide is designed for researchers, engineers, and scientists who need to configure ballistic simulation parameters without requiring knowledge of game development or Unreal Engine. The configuration system uses self-documenting JSON format with embedded explanations, making it accessible to ballistics researchers from any background. + +## Getting Started + +### Understanding the JSON Structure + +Each configuration file follows this pattern: +```json +{ + "ParameterName": { + "_description": "What this parameter does", + "_unit": "Units of measurement", + "_typical_values": "Expected ranges", + "_examples": "Real-world examples", + "value": actual_numeric_value + } +} +``` + +**Important**: The underscore-prefixed fields (`_description`, `_unit`, etc.) are documentation only. The `value` field contains the actual parameter that affects the simulation. + +### File Types + +1. **Bullet Properties** (`Sample_BulletProperties.json`) - Physical and ballistic characteristics of projectiles +2. **Material Response** (`Sample_MaterialResponse.json`) - How materials respond to ballistic impact + +## Bullet Properties Configuration + +### Essential Parameters + +#### Mass and Dimensions +- **GrainWeight**: Projectile mass in grains (1 grain = 0.0647989 grams) + - Small caliber: 20-80 grains + - Medium caliber: 80-200 grains + - Large caliber: 200-750+ grains + +- **DiameterInches**: Bullet diameter in inches + - Common values: 0.224" (5.56mm), 0.308" (7.62mm), 0.452" (45 ACP) + +- **LengthInches**: Overall bullet length + - Typically 0.4-2.0 inches depending on caliber + +#### Ballistic Coefficients +Choose **either** G1 or G7 model (set UseG7Model accordingly): + +- **G1 Model**: Traditional standard, good for round-nose bullets + - Poor: 0.15-0.25 + - Average: 0.25-0.45 + - Excellent: 0.65+ + +- **G7 Model**: Better for modern pointed bullets + - Typically 0.5-0.7× the G1 value + - More accurate for boat-tail designs + +#### Material Properties +- **BulletHardness**: Brinell Hardness Number + - Lead: 5-15 BHN + - Copper jacket: 15-25 BHN + - Steel core: 150-600 BHN + +- **PenetrationEnergyThreshold**: Minimum energy for penetration (ft-lbs) + - Varies by intended use: 10-500+ ft-lbs + +### Research Applications + +#### Ballistics Research +For academic ballistics research, focus on: +- Accurate ballistic coefficients from wind tunnel data +- Precise mass and dimensional measurements +- Material hardness from standardized testing + +#### Terminal Ballistics Studies +For penetration and wound ballistics: +- Expansion characteristics (velocity thresholds, max expansion) +- Energy transfer parameters +- Material-specific hardness values + +#### Forensic Applications +For bullet identification and trajectory analysis: +- Precise dimensional measurements +- Material composition data +- Manufacturer-specific variations + +## Material Response Configuration + +### Penetration Mechanics + +#### Basic Penetration +- **PenetrationDepthMultiplier**: Scaling factor (0.1-10.0) + - Use 1.0 as baseline, adjust based on experimental data + - Values <1.0 for harder materials, >1.0 for softer + +- **PenetrationNormalization**: Angle dependency (degrees) + - 0-15°: Soft materials (wood, plastic) + - 15-30°: Medium materials (aluminum, mild steel) + - 30-60°: Hard materials (armor steel, ceramics) + +#### Advanced Penetration Modeling +When **UseMathematicalProperties** is enabled: + +- **DensityGPerCm3**: Material density from engineering handbooks +- **TensileStrengthMPa**: Ultimate tensile strength from material testing +- **YieldStrengthMPa**: Yield strength (typically 50-80% of tensile strength) +- **BallisticLimitVelocity**: V50 velocity from ballistic testing + +### Ricochet Behavior + +#### Key Parameters +- **RicochetProbabilityMultiplier**: Likelihood scaling (0.0-5.0) + - 0.0: No ricochets (soft materials) + - 1.0: Normal probability + - 2.0+: High ricochet materials (hardened steel, ice) + +- **RicochetRestitution**: Energy retention (0.0-1.0) + - Soft materials: 0.1-0.3 + - Hard materials: 0.7-0.9 + +### Spalling and Fragmentation + +#### When to Enable Spalling +Spalling is relevant for: +- Brittle materials (concrete, ceramics, glass) +- High-energy impacts (>2000 fps) +- Research into secondary fragmentation effects + +#### Critical Parameters +- **SpallVelocityThreshold**: Minimum impact velocity (cm/s) + - Glass: 15,000-30,000 cm/s + - Concrete: 30,000-60,000 cm/s + - Steel: 60,000+ cm/s + +- **SpallFragmentCount**: Number of fragments (1-20) + - Balance realism vs. computational cost + - Typical: 3-8 fragments for most materials + +## Data Sources and Validation + +### Recommended References + +#### Bullet Properties +1. **Manufacturer Data**: Official ballistic coefficients and specifications +2. **Ballistic Tables**: Sierra, Hornady, Nosler reloading manuals +3. **Military Standards**: NATO STANAG documents for military ammunition +4. **SAAMI Standards**: Sporting Arms and Ammunition Manufacturers' Institute + +#### Material Properties +1. **ASTM Standards**: Material testing standards (E8, E10, etc.) +2. **Engineering Handbooks**: ASM Metals Handbook, Machinery's Handbook +3. **Research Literature**: Journal of Applied Physics, International Journal of Impact Engineering +4. **Government Databases**: NIST materials database, military research reports + +### Experimental Validation + +#### Ballistic Testing +- Use chronographs for velocity measurements +- Ballistic gelatin for terminal performance +- Steel plate testing for penetration limits +- High-speed photography for fragment analysis + +#### Material Testing +- Tensile testing per ASTM E8 +- Hardness testing per ASTM E10 +- Impact testing per ASTM E23 +- Ballistic limit testing per MIL-STD protocols + +## Common Material Examples + +### Metals +```json +// Mild Steel (A36) +"DensityGPerCm3": 7.85, +"MaterialHardness": 150.0, +"TensileStrengthMPa": 400.0, +"YieldStrengthMPa": 250.0 + +// Aluminum 6061-T6 +"DensityGPerCm3": 2.70, +"MaterialHardness": 95.0, +"TensileStrengthMPa": 310.0, +"YieldStrengthMPa": 276.0 + +// Hardened Steel (Armor) +"DensityGPerCm3": 7.85, +"MaterialHardness": 500.0, +"TensileStrengthMPa": 1200.0, +"YieldStrengthMPa": 1000.0 +``` + +### Ceramics and Composites +```json +// Alumina Ceramic +"DensityGPerCm3": 3.95, +"MaterialHardness": 1500.0, +"TensileStrengthMPa": 300.0, +"YieldStrengthMPa": 300.0, +"EnableSpalling": true + +// Kevlar Composite +"DensityGPerCm3": 1.44, +"MaterialHardness": 50.0, +"TensileStrengthMPa": 3500.0, +"YieldStrengthMPa": 3500.0, +"NeverRicochet": true +``` + +## Quality Assurance + +### Parameter Validation +1. **Physical Consistency**: Ensure yield strength ≤ tensile strength +2. **Unit Consistency**: Verify all units match the documentation +3. **Range Checking**: Compare values against published material databases +4. **Cross-Reference**: Validate against multiple independent sources + +### Simulation Validation +1. **Known Results**: Test against published ballistic data +2. **Sensitivity Analysis**: Vary parameters to understand system response +3. **Comparative Testing**: Compare results between different bullet/material combinations +4. **Literature Comparison**: Validate results against peer-reviewed research + +## JSON Configuration Examples + +### Complete Bullet Properties Example +```json +{ + "bulletProperties": { + "grainWeight": { + "_description": "Projectile mass in grains", + "_unit": "grains", + "_typical_values": "20-750 grains depending on caliber", + "_examples": "55gr for 5.56mm, 150gr for .308", + "value": 55.0 + }, + "diameterInches": { + "_description": "Bullet diameter in inches", + "_unit": "inches", + "_typical_values": "0.17-0.8 inches", + "_examples": "0.224 for 5.56mm, 0.308 for 7.62mm", + "value": 0.224 + }, + "lengthInches": { + "_description": "Overall bullet length", + "_unit": "inches", + "_typical_values": "0.4-2.0 inches", + "_examples": "0.825 for M855, 1.35 for match bullets", + "value": 0.825 + }, + "ballisticCoefficientG1": { + "_description": "Aerodynamic efficiency using G1 standard", + "_unit": "dimensionless", + "_typical_values": "0.15-0.8", + "_examples": "0.151 for M855, 0.6+ for match bullets", + "value": 0.151 + }, + "bulletHardness": { + "_description": "Brinell hardness number", + "_unit": "BHN", + "_typical_values": "5-600 BHN", + "_examples": "15 for copper jacket, 300+ for steel core", + "value": 15.0 + } + }, + "metadata": { + "description": "5.56x45mm NATO M855 Ball", + "source": "Military specification data", + "date": "2024-12-02", + "researcher": "Ballistics Lab" + } +} +``` + +### Complete Material Response Example +```json +{ + "materialResponse": { + "basicProperties": { + "neverPenetrate": { + "_description": "Material never allows penetration", + "_unit": "boolean", + "_typical_values": "false for most materials", + "_examples": "true for backstop materials", + "value": false + }, + "penetrationDepthMultiplier": { + "_description": "Penetration resistance scaling", + "_unit": "dimensionless", + "_typical_values": "0.1-10.0", + "_examples": "0.3 for steel, 2.0 for wood", + "value": 1.0 + } + }, + "mathematicalProperties": { + "densityGPerCm3": { + "_description": "Material density", + "_unit": "g/cm³", + "_typical_values": "0.1-20.0", + "_examples": "7.85 for steel, 2.70 for aluminum", + "value": 7.85 + }, + "tensileStrengthMPa": { + "_description": "Ultimate tensile strength", + "_unit": "MPa", + "_typical_values": "1-5000 MPa", + "_examples": "400 for mild steel, 3500 for Kevlar", + "value": 400.0 + }, + "yieldStrengthMPa": { + "_description": "Yield strength (must be ≤ tensile)", + "_unit": "MPa", + "_typical_values": "50-80% of tensile strength", + "_examples": "250 for mild steel with 400 MPa tensile", + "value": 250.0 + } + } + }, + "metadata": { + "materialType": "Mild Steel A36", + "standard": "ASTM A36", + "source": "ASM Metals Handbook", + "date": "2024-12-02" + } +} +``` + +## Troubleshooting + +### Common Issues +1. **Unrealistic Penetration**: Check density and hardness values +2. **No Expansion**: Verify expansion velocity threshold vs. impact velocity +3. **Excessive Ricochets**: Reduce ricochet probability multiplier +4. **Performance Issues**: Reduce spalling fragment count and density + +### Best Practices +1. **Start Simple**: Begin with basic parameters, add complexity gradually +2. **Document Sources**: Record where each value came from for traceability +3. **Version Control**: Keep track of parameter changes and their effects +4. **Peer Review**: Have other researchers validate your parameter choices + +## Integration with Research Workflows + +### Data Pipeline +1. **Experimental Data** → Material testing, ballistic testing +2. **Literature Review** → Published coefficients, industry standards +3. **Configuration Files** → JSON parameter files +4. **Simulation** → EasyBallistics simulation runs +5. **Analysis** → Results validation and publication + +### Version Control for Research +```json +{ + "versionControl": { + "configurationVersion": "1.2.0", + "changeLog": [ + { + "version": "1.2.0", + "date": "2024-12-02", + "changes": ["Updated steel hardness based on new testing"], + "researcher": "Dr. Smith" + }, + { + "version": "1.1.0", + "date": "2024-11-15", + "changes": ["Added ceramic spalling parameters"], + "researcher": "Lab Team" + } + ] + } +} +``` + +### Collaboration Features +- **Shared Parameter Libraries**: Common configurations for research groups +- **Validation Protocols**: Standardized testing procedures +- **Result Archival**: Maintaining configuration-result relationships +- **Publication Support**: Traceable parameters for research papers + +## Advanced Research Applications + +### Multi-Material Studies +For composite armor or layered materials: +- Configure each layer separately +- Account for interface effects +- Consider cumulative energy absorption + +### Dynamic Effects +For high strain rate phenomena: +- Use dynamic strength values (typically 1.5-3× static) +- Account for temperature effects +- Consider strain rate sensitivity + +### Statistical Analysis +For probabilistic studies: +- Use parameter ranges rather than single values +- Apply Monte Carlo methods +- Document uncertainty and sensitivity + +### Experimental Design +For systematic studies: +- Design of experiments (DOE) methodology +- Parameter space exploration +- Sensitivity analysis protocols + +## Sample JSON Files + +The EasyBallistics package includes sample JSON files that demonstrate proper configuration: + +- **Sample_BulletProperties.json**: Example bullet configurations for common calibers +- **Sample_MaterialResponse.json**: Example material response configurations + +These files can be found in the `Documentation/` directory and serve as templates for your own research configurations. + +## See Also + +- [Parameter Validation Reference](../api/parameter-validation-reference) - Complete validation guidelines +- [JSON Import/Export Tool](json-import-export) - Tool for managing configurations +- [Material Properties Reference](../api/material-properties-reference) - Complete material property API +- [Mathematical Ballistics](../core-concepts/mathematical-ballistics) - Understanding the physics + +--- + +*This configuration system is designed to support serious ballistics research while maintaining computational efficiency. The self-documenting JSON format ensures that parameter choices are traceable and scientifically justified.* \ No newline at end of file diff --git a/docs/docs/api/parameter-validation-reference.md b/docs/docs/api/parameter-validation-reference.md new file mode 100644 index 0000000..7e09a1e --- /dev/null +++ b/docs/docs/api/parameter-validation-reference.md @@ -0,0 +1,324 @@ +# Parameter Validation Reference + +## Overview + +This document provides comprehensive validation ranges, typical values, and quality assurance guidelines for ballistic simulation parameters. Use this reference to verify that your configuration values are physically realistic and within expected bounds. + +## Bullet Properties Validation + +### Physical Dimensions + +#### Grain Weight Validation +| Caliber Category | Typical Range (grains) | Examples | +|------------------|------------------------|----------| +| Rimfire | 20-60 | .22 LR: 30-40 gr | +| Small Pistol | 60-130 | 9mm: 115-147 gr | +| Large Pistol | 130-300 | .45 ACP: 200-230 gr | +| Small Rifle | 40-90 | .223: 55-77 gr | +| Medium Rifle | 100-200 | .308: 150-180 gr | +| Large Rifle | 200-500 | .338: 250-300 gr | +| Anti-Material | 500-750+ | .50 BMG: 647-750 gr | + +**Quality Checks:** +- Weight should correlate with caliber size +- Unusually light bullets (<20 gr) may indicate frangible/training rounds +- Unusually heavy bullets (>300 gr for caliber) may indicate specialty rounds + +#### Diameter Validation +| Caliber | Actual Diameter (inches) | Metric Equivalent | +|---------|--------------------------|-------------------| +| .17 | 0.172 | 4.37mm | +| .22 | 0.224 | 5.69mm | +| .24 | 0.243 | 6.17mm | +| .25 | 0.257 | 6.53mm | +| .27 | 0.277 | 7.04mm | +| .28 | 0.284 | 7.21mm | +| .30 | 0.308 | 7.82mm | +| .32 | 0.312 | 7.92mm | +| .35 | 0.358 | 9.09mm | +| .38 | 0.357 | 9.07mm | +| .40 | 0.400 | 10.16mm | +| .44 | 0.429 | 10.90mm | +| .45 | 0.452 | 11.48mm | +| .50 | 0.510 | 12.95mm | + +**Quality Checks:** +- Diameter must be positive and reasonable (0.1-0.8 inches typical) +- Should match known caliber specifications +- Metric conversions should be consistent + +#### Length-to-Diameter Ratio +| Bullet Type | Typical L/D Ratio | Length Range (inches) | +|-------------|-------------------|----------------------| +| Round Nose | 1.5-2.5 | Short, traditional design | +| Spitzer | 2.5-4.0 | Pointed, aerodynamic | +| Boat Tail | 3.0-4.5 | Long, high BC | +| VLD | 4.0-6.0 | Very Low Drag, match | +| Solid | 2.0-5.0 | Monolithic construction | + +**Quality Checks:** +- L/D ratio should be realistic for bullet type +- Very long bullets (L/D > 6) may have stability issues +- Very short bullets (L/D < 1.5) are unusual outside specialty applications + +### Ballistic Coefficients + +#### G1 Ballistic Coefficient Ranges +| Bullet Shape | BC Range | Description | +|--------------|----------|-------------| +| Round Nose | 0.15-0.25 | Traditional, poor aerodynamics | +| Flat Point | 0.20-0.30 | Lever gun bullets | +| Spitzer | 0.25-0.45 | Standard pointed bullets | +| Boat Tail | 0.35-0.55 | Improved base design | +| Match/VLD | 0.45-0.70+ | Optimized for accuracy | +| Ultra-High BC | 0.70-1.0+ | Specialized long-range | + +#### G7 to G1 Conversion +- G7 BC typically 0.5-0.7× the G1 value for modern bullets +- Conversion varies by bullet shape and velocity regime +- G7 more accurate for boat-tail designs + +**Quality Checks:** +- BC should correlate with bullet shape and quality +- Extremely high BC (>0.8 G1) requires verification +- G7 values should be lower than corresponding G1 values + +### Material Properties + +#### Bullet Hardness (BHN) +| Material | Hardness Range | Applications | +|----------|----------------|--------------| +| Pure Lead | 5-8 BHN | Cast bullets, training | +| Wheel Weights | 12-15 BHN | Economy cast bullets | +| Linotype | 22-25 BHN | Hard cast bullets | +| Swaged Lead | 8-12 BHN | Commercial lead bullets | +| Copper Plated | 10-15 BHN | Plated lead core | +| Copper Jacket | 15-25 BHN | FMJ bullets | +| Brass | 60-120 BHN | Solid brass bullets | +| Copper Solid | 35-80 BHN | Monolithic copper | +| Steel Core | 150-300 BHN | AP bullets | +| Tungsten | 300-400 BHN | High-density cores | + +**Quality Checks:** +- Hardness should match material type +- Jacket hardness is composite value including core +- Extremely hard bullets (>400 BHN) are specialized + +#### Energy and Velocity Thresholds + +**Penetration Energy Threshold (ft-lbs)** +| Application | Energy Range | Examples | +|-------------|--------------|----------| +| Small Game | 10-25 | Squirrel, rabbit | +| Varmint | 25-75 | Prairie dog, coyote | +| Deer | 75-150 | White-tail deer | +| Elk | 150-300 | Large game | +| Dangerous Game | 300-500+ | Bear, buffalo | +| Armor Piercing | 500-2000+ | Military applications | + +**Expansion Velocity Threshold (fps)** +| Bullet Type | Velocity Range | Notes | +|-------------|----------------|-------| +| Handgun HP | 800-1200 | Lower velocity expansion | +| Rifle HP | 1600-2200 | Standard hunting bullets | +| Premium Hunting | 1400-1800 | Controlled expansion | +| Varmint | 2000-3000 | Rapid expansion | +| Match | N/A | Non-expanding | + +**Quality Checks:** +- Energy thresholds should match intended use +- Expansion velocities should be achievable at impact range +- Thresholds should be lower than typical muzzle velocities + +## Material Response Validation + +### Physical Properties + +#### Density Values (g/cm³) +| Material Category | Density Range | Common Materials | +|-------------------|---------------|------------------| +| Woods | 0.3-1.2 | Balsa (0.16), Oak (0.75), Ebony (1.2) | +| Plastics | 0.9-2.0 | PE (0.95), Nylon (1.15), PVC (1.4) | +| Aluminum Alloys | 2.6-2.8 | 1100 (2.71), 6061 (2.70), 7075 (2.81) | +| Concrete | 2.0-2.8 | Normal (2.4), High-strength (2.6) | +| Titanium Alloys | 4.4-4.9 | Ti-6Al-4V (4.43), CP Ti (4.51) | +| Steel | 7.7-8.1 | Mild (7.85), Stainless (8.0) | +| Lead | 11.3-11.4 | Pure lead (11.34) | +| Tungsten | 19.2-19.3 | Pure tungsten (19.25) | + +#### Hardness Correlation +**Brinell Hardness (HB)** +| Material | Hardness Range | Strength Correlation | +|----------|----------------|---------------------| +| Aluminum (soft) | 15-30 | Low strength | +| Aluminum (hard) | 60-150 | Heat treated | +| Steel (mild) | 120-200 | Structural steel | +| Steel (medium) | 200-300 | Heat treated | +| Steel (hard) | 300-500 | Tool steel | +| Steel (very hard) | 500-700 | Hardened/tempered | +| Ceramics | 1000-2000+ | Very brittle | + +**Quality Checks:** +- Hardness should correlate with tensile strength +- Rule of thumb: BHN ≈ 3.45 × Tensile Strength (ksi) +- Very hard materials often brittle + +#### Strength Properties (MPa) + +**Tensile Strength Ranges** +| Material Class | Yield Strength | Tensile Strength | Ratio (Yield/Tensile) | +|----------------|----------------|------------------|----------------------| +| Aluminum (soft) | 35-100 | 90-200 | 0.4-0.5 | +| Aluminum (hard) | 200-500 | 300-600 | 0.6-0.8 | +| Steel (mild) | 200-400 | 400-600 | 0.5-0.7 | +| Steel (high strength) | 400-1000 | 600-1200 | 0.7-0.8 | +| Stainless Steel | 200-800 | 500-1000 | 0.4-0.8 | +| Titanium | 200-1000 | 300-1200 | 0.6-0.8 | + +**Quality Checks:** +- Yield strength must be ≤ tensile strength +- Typical ratio: yield = 50-80% of tensile +- Higher ratios indicate brittle materials + +### Ballistic Properties + +#### Ballistic Limit Velocities (fps) +| Material/Thickness | Velocity Range | Projectile Type | +|-------------------|----------------|-----------------| +| Aluminum 0.25" | 800-1500 | Standard ball | +| Steel 0.25" | 1500-2500 | Standard ball | +| Steel 0.5" | 2500-3500 | Standard ball | +| Kevlar vest | 1200-1800 | Handgun bullets | +| Ceramic tile | 2000-3500 | Rifle bullets | +| Glass | 200-800 | Low-velocity impacts | + +#### Energy Absorption Coefficients +| Material Type | Coefficient Range | Mechanism | +|---------------|-------------------|-----------| +| Soft (foam, sand) | 0.8-1.0 | Compression/displacement | +| Wood/Plastic | 0.6-0.8 | Crushing/deformation | +| Aluminum | 0.5-0.7 | Plastic deformation | +| Mild Steel | 0.4-0.6 | Plastic deformation | +| Hard Steel | 0.2-0.4 | Limited deformation | +| Ceramics | 0.3-0.5 | Fracture/pulverization | + +### Ricochet Properties + +#### Ricochet Probability by Material +| Surface Type | Probability Multiplier | Impact Angle Dependency | +|--------------|------------------------|------------------------| +| Water | 0.3-0.7 | High angle dependency | +| Concrete | 0.8-1.2 | Moderate dependency | +| Steel (mild) | 1.0-1.5 | Low angle dependency | +| Steel (hard) | 1.2-2.0 | Very low dependency | +| Ice | 1.2-1.8 | Temperature dependent | +| Rock/Stone | 0.6-1.4 | Surface roughness dependent | + +#### Restitution Coefficients +| Material Pair | Coefficient Range | Energy Retention | +|---------------|-------------------|------------------| +| Lead-Steel | 0.2-0.4 | Low retention | +| Copper-Steel | 0.3-0.5 | Moderate retention | +| Steel-Steel | 0.5-0.7 | High retention | +| Tungsten-Steel | 0.6-0.8 | Very high retention | + +### Spalling Parameters + +#### Velocity Thresholds by Material +| Material | Threshold (cm/s) | Threshold (fps) | Fragment Characteristics | +|----------|------------------|-----------------|-------------------------| +| Glass | 15,000-30,000 | 500-1000 | Sharp, dangerous | +| Concrete | 30,000-60,000 | 1000-2000 | Angular, dust | +| Aluminum | 60,000-90,000 | 2000-3000 | Metal flakes | +| Mild Steel | 90,000-150,000 | 3000-5000 | Metal fragments | +| Hard Steel | 150,000+ | 5000+ | Small, high-velocity | + +#### Fragment Count Guidelines +| Impact Energy | Fragment Count | Material Type | +|---------------|----------------|---------------| +| Low (<1000 ft-lbs) | 1-3 | Brittle materials only | +| Medium (1000-5000) | 3-8 | Most materials | +| High (5000-15000) | 8-15 | High-energy impacts | +| Very High (>15000) | 15-25 | Extreme conditions | + +**Quality Checks:** +- Fragment count should scale with impact energy +- Brittle materials generate more fragments +- Consider computational cost vs. realism + +## Validation Procedures + +### Cross-Reference Validation +1. **Material Databases**: ASM International, NIST +2. **Military Standards**: MIL-HDBK, NATO STANAGs +3. **Industry Standards**: ASTM, SAE, AISI +4. **Academic Literature**: Peer-reviewed journals + +### Physical Consistency Checks +1. **Density-Strength Correlation**: Generally, denser materials are stronger +2. **Hardness-Strength Relationship**: BHN ≈ 3.45 × UTS (ksi) +3. **Yield-Tensile Ratio**: Yield typically 50-80% of tensile strength +4. **Elastic Modulus**: Should match material class + +### Ballistic Reasonableness +1. **BC vs. Shape**: High BC requires good aerodynamic shape +2. **Energy vs. Caliber**: Larger calibers generally have more energy +3. **Velocity Thresholds**: Should be achievable at intended range +4. **Material Response**: Should match known ballistic test results + +### Common Validation Errors + +#### Bullet Properties +- **Impossible BC**: Values >1.0 require careful verification +- **Mismatched Dimensions**: Diameter doesn't match caliber designation +- **Unrealistic Hardness**: Values inconsistent with material type +- **Inappropriate Thresholds**: Energy/velocity values too high/low for application + +#### Material Properties +- **Inverted Strength Values**: Yield > tensile strength +- **Impossible Density**: Values outside known material ranges +- **Inconsistent Hardness**: Doesn't correlate with strength values +- **Unrealistic Ballistic Limits**: Too high/low for material thickness + +### Quality Assurance Checklist + +#### Before Simulation +- [ ] All values within documented ranges +- [ ] Physical relationships consistent (yield ≤ tensile, etc.) +- [ ] Units correct and consistent +- [ ] Sources documented for traceability +- [ ] Cross-referenced with multiple sources + +#### During Simulation +- [ ] Results match expected physical behavior +- [ ] No obvious artifacts or anomalies +- [ ] Performance acceptable for application +- [ ] Sensitivity analysis completed + +#### After Simulation +- [ ] Results compared to experimental data when available +- [ ] Peer review of critical parameters +- [ ] Documentation updated with lessons learned +- [ ] Parameters archived for reproducibility + +## Recommended References + +### Primary Sources +1. **ASM International Handbook Series** - Comprehensive material properties +2. **Military Ballistics Handbooks** - Validated ballistic data +3. **NIST Material Database** - Standardized property values +4. **Manufacturer Specifications** - Bullet and ammunition data + +### Academic Journals +1. **International Journal of Impact Engineering** +2. **Journal of Applied Physics** +3. **Experimental Mechanics** +4. **Defence Technology** + +### Professional Organizations +1. **SAAMI** - Sporting ammunition standards +2. **NATO** - Military ammunition specifications +3. **ASTM International** - Testing standards +4. **ASM International** - Materials engineering + +This validation reference ensures that ballistic simulations are based on physically realistic and well-documented parameters, supporting credible research and engineering applications. \ No newline at end of file diff --git a/docs/docs/tutorials/gun-blueprint-guide.md b/docs/docs/tutorials/gun-blueprint-guide.md index 212bac9..55e736a 100644 --- a/docs/docs/tutorials/gun-blueprint-guide.md +++ b/docs/docs/tutorials/gun-blueprint-guide.md @@ -2,12 +2,17 @@ This guide covers creating fully functional gun blueprints using EasyBallistics' new **Gun-Centric Architecture**. The `AEBGun` class provides a complete weapon system with integrated barrel, magazine, and ammunition management. +> **🎨 Interactive Blueprint Visualization** +> +> This guide uses **[Klee](https://joined-forces.github.io/klee/)** for interactive Blueprint visualization. You can view, share, and experiment with the Blueprint examples directly in your browser. All Blueprint code snippets can be copied and visualized using Klee's web interface. + ## Prerequisites - Basic knowledge of Unreal Engine Blueprints - Understanding of Data Assets and Actor Components - Familiarity with Unreal's event system - EasyBallistics plugin installed and enabled +- **Optional**: [Klee](https://joined-forces.github.io/klee/) for Blueprint visualization and sharing ## Architecture Overview @@ -119,119 +124,183 @@ The Gun-Centric Architecture consists of: #### A. Basic Initialization -```blueprint -Event BeginPlay -├── Apply Weapon Configuration -│ └── Weapon Config: BP_M4_WeaponConfig -├── Load Magazine -│ └── Bullet Types: [BP_556NATO_BulletProperties x30] -├── Charging Handle -│ └── (Chambers first round) -└── Bind Events - ├── Bind Event to OnGunFired - ├── Bind Event to OnMagazineEmpty - ├── Bind Event to OnGunStateChanged - └── Bind Event to OnSafetyChanged +**Blueprint Graph**: [View in Klee](https://joined-forces.github.io/klee/) + +```unreal +Begin Object Class=/Script/BlueprintGraph.K2Node_Event Name="K2Node_Event_BeginPlay" +EventReference=(MemberParent=Class'"/Script/Engine.Actor"',MemberName="ReceiveBeginPlay") +bOverrideFunction=True +NodeGuid=DA78E90542B5EA6FFF2BB580420BADA4 +CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec") +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_ApplyWeaponConfig" +FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="ApplyWeaponConfiguration") +NodePosX=288 +NodeGuid=D94D72E348471465CF685AB3C289A168 +CustomProperties Pin (PinName="WeaponConfig",PinType.PinCategory="object",DefaultValue="BP_M4_WeaponConfig") +End Object ``` +> **📋 Blueprint Workflow**: +> 1. **Event BeginPlay**: Triggers when the gun spawns +> 2. **Apply Weapon Configuration**: Sets up all gun parameters from the data asset +> 3. **Load Magazine**: Fills magazine with specified bullet types +> 4. **Charging Handle**: Chambers the first round +> 5. **Bind Events**: Connects gun events to response functions + #### B. Input Actions -```blueprint -InputAction Fire (Pressed) -├── Branch (Can Fire) -│ └── True: Pull Trigger -└── False: Print String "Cannot Fire" +**Fire Input Blueprint**: [View in Klee](https://joined-forces.github.io/klee/) -InputAction Fire (Released) -└── Release Trigger +```unreal +Begin Object Class=/Script/BlueprintGraph.K2Node_InputAction Name="K2Node_InputAction_Fire" +InputActionName="Fire" +bConsumeInput=True +NodeGuid=1A2B3C4D5E6F7890ABCDEF1234567890 +CustomProperties Pin (PinName="Pressed",Direction="EGPD_Output",PinType.PinCategory="exec") +CustomProperties Pin (PinName="Released",Direction="EGPD_Output",PinType.PinCategory="exec") +End Object -InputAction Reload (Pressed) -├── Branch (Rounds in Magazine < Max Capacity) -│ └── True: Reload Sequence -└── False: Print String "Magazine Full" +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_CanFire" +FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="CanFire") +NodePosX=288 +NodeGuid=2B3C4D5E6F7890ABCDEF1234567890AB +CustomProperties Pin (PinName="ReturnValue",Direction="EGPD_Output",PinType.PinCategory="bool") +End Object -InputAction ToggleSafety (Pressed) -├── Get Safety State -├── NOT (Safety State) -└── Set Safety (Result) - -InputAction ToggleFireMode (Pressed) -├── Get Fire Mode -├── Switch on Enum (Fire Mode) -│ ├── Semi-Auto: Set Fire Mode (Full Auto) -│ ├── Full Auto: Set Fire Mode (Semi-Auto) -│ └── Default: Set Fire Mode (Semi-Auto) -└── Print String ("Fire Mode: " + New Mode) +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_PullTrigger" +FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="PullTrigger") +NodePosX=576 +NodeGuid=3C4D5E6F7890ABCDEF1234567890ABCD +End Object ``` +> **🎮 Input Mapping**: +> - **Fire (Pressed)**: Check `Can Fire` → `Pull Trigger` if valid +> - **Fire (Released)**: Always call `Release Trigger` +> - **Reload**: Check magazine capacity → Execute reload sequence +> - **Toggle Safety**: Get current state → Set opposite state +> - **Toggle Fire Mode**: Cycle through available fire modes + +**Copy this Blueprint code and paste it into [Klee](https://joined-forces.github.io/klee/) to see the visual graph!** + #### C. Reload System -```blueprint -Custom Event: Reload Sequence -├── Branch (Has Ammo in Inventory) -│ ├── True: Perform Reload -│ │ ├── Eject Magazine -│ │ ├── Delay (Reload Animation Time) -│ │ ├── Insert Magazine -│ │ │ └── New Magazine: Create New Magazine -│ │ ├── Charging Handle -│ │ └── Play Reload Sound -│ └── False: Play Empty Sound -└── Update Ammo UI +**Reload Blueprint**: [View in Klee](https://joined-forces.github.io/klee/) + +```unreal +Begin Object Class=/Script/BlueprintGraph.K2Node_CustomEvent Name="K2Node_ReloadSequence" +CustomFunctionName="Reload Sequence" +NodeGuid=4D5E6F7890ABCDEF1234567890ABCDEF +CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec") +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_EjectMagazine" +FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="EjectMagazine") +NodePosX=288 +NodeGuid=5E6F7890ABCDEF1234567890ABCDEF12 +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_InsertMagazine" +FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="InsertMagazine") +NodePosX=576 +NodeGuid=6F7890ABCDEF1234567890ABCDEF1234 +CustomProperties Pin (PinName="NewMagazine",PinType.PinCategory="object") +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_ChargingHandle" +FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="ChargingHandle") +NodePosX=864 +NodeGuid=7890ABCDEF1234567890ABCDEF123456 +End Object ``` +> **🔄 Reload Workflow**: +> 1. **Check Inventory**: Verify player has ammunition available +> 2. **Eject Magazine**: Remove current magazine (empty or partial) +> 3. **Animation Delay**: Wait for reload animation timing +> 4. **Insert Magazine**: Add new magazine with fresh ammunition +> 5. **Charging Handle**: Chamber first round from new magazine +> 6. **Audio Feedback**: Play reload completion sound + +**Visualization Tip**: Use [Klee](https://joined-forces.github.io/klee/) to see how the reload sequence flows and branches based on different conditions! + ### 4. Event Response System #### A. Gun State Management -```blueprint -OnGunStateChanged (NewState) -├── Switch on Enum (NewState) -│ ├── Ready: -│ │ ├── Set UI Color (Green) -│ │ ├── Hide Loading Indicator -│ │ └── Enable Fire Input -│ ├── Firing: -│ │ ├── Set UI Color (Red) -│ │ ├── Show Firing Indicator -│ │ └── Play Firing Animation -│ ├── Empty: -│ │ ├── Set UI Color (Gray) -│ │ ├── Show Reload Prompt -│ │ └── Disable Fire Input -│ ├── Jammed: -│ │ ├── Set UI Color (Yellow) -│ │ ├── Show Jam Indicator -│ │ └── Disable Fire Input -│ └── Safety On: -│ ├── Set UI Color (Orange) -│ ├── Show Safety Indicator -│ └── Disable Fire Input -└── Update Status Text +**State Management Blueprint**: [View in Klee](https://joined-forces.github.io/klee/) + +```unreal +Begin Object Class=/Script/BlueprintGraph.K2Node_Event Name="K2Node_OnGunStateChanged" +EventReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="OnGunStateChanged") +NodeGuid=8901ABCDEF1234567890ABCDEF123456 +CustomProperties Pin (PinName="NewState",PinType.PinCategory="byte",PinType.PinSubCategoryObject=Enum'"/Script/EasyBallistics.EGunState"') +CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec") +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_Switch Name="K2Node_SwitchGunState" +IndexPinType.PinCategory="byte" +IndexPinType.PinSubCategoryObject=Enum'"/Script/EasyBallistics.EGunState"' +NodePosX=288 +NodeGuid=9012ABCDEF1234567890ABCDEF123456 +CustomProperties Pin (PinName="Ready",Direction="EGPD_Output",PinType.PinCategory="exec") +CustomProperties Pin (PinName="Firing",Direction="EGPD_Output",PinType.PinCategory="exec") +CustomProperties Pin (PinName="Empty",Direction="EGPD_Output",PinType.PinCategory="exec") +CustomProperties Pin (PinName="Jammed",Direction="EGPD_Output",PinType.PinCategory="exec") +CustomProperties Pin (PinName="SafetyOn",Direction="EGPD_Output",PinType.PinCategory="exec") +End Object ``` +> **🎯 State Visualization**: +> - **Ready** (Green): Gun can fire normally +> - **Firing** (Red): Gun is actively firing +> - **Empty** (Gray): No ammunition available +> - **Jammed** (Yellow): Malfunction occurred +> - **Safety On** (Orange): Safety engaged, cannot fire + +**Interactive Demo**: Paste this code into [Klee](https://joined-forces.github.io/klee/) to see how the state machine branches! + #### B. Firing Effects -```blueprint -OnGunFired (BulletType) -├── Spawn Emitter Attached -│ ├── Emitter Template: MuzzleFlash -│ ├── Attach to Component: GunMesh -│ └── Socket Name: MuzzleSocket -├── Play Sound 2D (GunFireSound) -├── Apply Camera Shake -│ ├── Shake Class: GunFireShake -│ └── Scale: 1.0 -├── Add Impulse to Player -│ ├── Impulse: Calculate Recoil Impulse -│ └── Velocity Change: false -├── Spawn Actor (ShellCasing) -│ ├── Class: BP_ShellCasing -│ ├── Transform: EjectionPort Socket -│ └── Spawn Even if Colliding: true -└── Update Ammo Counter +**Firing Effects Blueprint**: [View in Klee](https://joined-forces.github.io/klee/) + +```unreal +Begin Object Class=/Script/BlueprintGraph.K2Node_Event Name="K2Node_OnGunFired" +EventReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="OnGunFired") +NodeGuid=0123ABCDEF1234567890ABCDEF123456 +CustomProperties Pin (PinName="BulletType",PinType.PinCategory="object") +CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec") +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_SpawnEmitterAttached" +FunctionReference=(MemberParent=Class'"/Script/Engine.GameplayStatics"',MemberName="SpawnEmitterAttached") +NodePosX=288 +NodeGuid=1234ABCDEF1234567890ABCDEF123456 +CustomProperties Pin (PinName="EmitterTemplate",PinType.PinCategory="object",DefaultValue="MuzzleFlash") +CustomProperties Pin (PinName="AttachToComponent",PinType.PinCategory="object") +CustomProperties Pin (PinName="AttachPointName",PinType.PinCategory="name",DefaultValue="MuzzleSocket") +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_PlaySound2D" +FunctionReference=(MemberParent=Class'"/Script/Engine.GameplayStatics"',MemberName="PlaySound2D") +NodePosX=576 +NodeGuid=2345ABCDEF1234567890ABCDEF123456 +CustomProperties Pin (PinName="Sound",PinType.PinCategory="object",DefaultValue="GunFireSound") +End Object ``` +> **💥 Effects Chain**: +> 1. **Muzzle Flash**: Particle effect at barrel tip +> 2. **Audio**: Gunshot sound with 3D positioning +> 3. **Camera Shake**: Weapon recoil feedback +> 4. **Player Impulse**: Physical recoil force +> 5. **Shell Casing**: Ejected brass cartridge +> 6. **UI Update**: Ammunition counter refresh + +**Pro Tip**: Use [Klee's sharing feature](https://joined-forces.github.io/klee/) to share your custom firing effects with the community! + #### C. Magazine Management ```blueprint @@ -437,20 +506,33 @@ Manager: Weapon Pool #### A. Debug Visualization -```blueprint -Input Action: Toggle Debug -├── Branch (Debug Enabled) -│ ├── True: Disable Debug -│ │ ├── Clear Debug Display -│ │ └── Set Debug Enabled (false) -│ └── False: Enable Debug -│ ├── Show Trajectory -│ ├── Show Impact Points -│ ├── Show Weapon Stats -│ └── Set Debug Enabled (true) -└── Update Debug UI +**Debug System Blueprint**: [View in Klee](https://joined-forces.github.io/klee/) + +```unreal +Begin Object Class=/Script/BlueprintGraph.K2Node_InputAction Name="K2Node_ToggleDebug" +InputActionName="ToggleDebug" +bConsumeInput=True +NodeGuid=3456ABCDEF1234567890ABCDEF123456 +CustomProperties Pin (PinName="Pressed",Direction="EGPD_Output",PinType.PinCategory="exec") +End Object + +Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_SetBulletDebugCategory" +FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBBarrel"',MemberName="SetBulletDebugCategory") +NodePosX=576 +NodeGuid=4567ABCDEF1234567890ABCDEF123456 +CustomProperties Pin (PinName="CategoryName",PinType.PinCategory="string",DefaultValue="Trajectory") +CustomProperties Pin (PinName="bEnabled",PinType.PinCategory="bool",DefaultValue="true") +End Object ``` +> **🔍 Debug Features**: +> - **Trajectory Visualization**: See bullet paths in real-time +> - **Impact Markers**: Show where bullets hit surfaces +> - **Performance Metrics**: Monitor FPS and trace counts +> - **Weapon Statistics**: Display accuracy, recoil, etc. + +**Share Your Blueprints**: Export your debug setup and share it with [Klee](https://joined-forces.github.io/klee/) for collaborative debugging! + #### B. Test Scenarios ```blueprint @@ -524,10 +606,38 @@ Function: Run Weapon Tests 3. **Intuitive Controls**: Use standard FPS control schemes 4. **Accessibility**: Support different input methods +## Blueprint Sharing with Klee + +### Exporting Your Gun Blueprints + +1. **Copy Blueprint Text**: In Unreal Engine, select your Blueprint nodes and copy (`Ctrl+C`) +2. **Open Klee**: Navigate to [Klee](https://joined-forces.github.io/klee/) +3. **Paste Code**: Paste the Blueprint text into Klee's input field +4. **Generate Visualization**: Klee will create an interactive web visualization +5. **Share URL**: Copy the generated URL to share your Blueprint with others + +### Community Blueprint Gallery + +Visit the [Klee website](https://joined-forces.github.io/klee/) to explore community-shared Blueprints: +- **Weapon Systems**: Complete gun implementations +- **Fire Mode Variants**: Different firing mechanisms +- **Custom Ammunition**: Specialized bullet behaviors +- **Debug Utilities**: Helpful debugging tools + +### Interactive Learning + +Use Klee to: +- **Visualize Complex Logic**: See how event chains flow +- **Share Solutions**: Help others with Blueprint examples +- **Collaborate**: Work on multiplayer weapon systems together +- **Learn from Examples**: Study community implementations + ## Conclusion The Gun-Centric Architecture provides a comprehensive, realistic weapon system that handles all aspects of firearm behavior. By following this guide, you'll create fully functional weapons with proper ammunition management, multiple fire modes, and networking support. +**Enhanced with Klee**: All Blueprint examples in this guide can be visualized and shared using [Klee's web interface](https://joined-forces.github.io/klee/), making it easier to understand, debug, and collaborate on weapon systems. + The system is designed to be extensible, allowing you to add custom features while maintaining the core ballistic simulation accuracy that EasyBallistics is known for. -For additional examples and advanced techniques, refer to the other tutorials in this documentation set. \ No newline at end of file +For additional examples and advanced techniques, refer to the other tutorials in this documentation set. Don't forget to check out community-shared Blueprints on [Klee](https://joined-forces.github.io/klee/)! \ No newline at end of file diff --git a/docs/docs/tutorials/weapon-data-setup.md b/docs/docs/tutorials/weapon-data-setup.md new file mode 100644 index 0000000..a130d32 --- /dev/null +++ b/docs/docs/tutorials/weapon-data-setup.md @@ -0,0 +1,425 @@ +# Weapon Data Asset Setup Guide + +This guide walks you through creating and configuring weapon data assets using the modern AAA weapon system with advanced Niagara VFX integration. + +## Overview + +The EasyBallistics weapon system uses a **data-driven approach** where: +- **Single persistent weapon actor** handles all weapons +- **Weapon Data Assets** define all weapon properties +- **Hot-swappable configuration** allows instant weapon switching +- **Advanced Niagara VFX** provides AAA-quality visual effects + +--- + +## Creating a New Weapon Data Asset + +### Step 1: Create the Data Asset + +1. **Right-click** in the Content Browser +2. Navigate to **Miscellaneous > Data Asset** +3. Select **EBWeaponData** as the parent class +4. Name your asset (e.g., `DA_AssaultRifle_AK47`) + +### Step 2: Configure Basic Identity + +Open your weapon data asset and configure the **Identity** section: + +``` +Weapon Name: "AK-47" +Weapon Description: "7.62x39mm assault rifle with high damage and moderate recoil" +Weapon Type: Rifle +Weapon Icon: [Select texture asset] +Weapon Tier: 3 +``` + +--- + +## Visual Configuration + +### Mesh Setup + +Configure the weapon's visual representation: + +``` +First Person Mesh: SK_AK47_FP (first-person skeletal mesh) +Third Person Mesh: SK_AK47_TP (third-person skeletal mesh) +Static Mesh: SM_AK47_Static (fallback static mesh) +Material Overrides: [Optional material variations] +``` + +**Required Sockets:** +- `MuzzleSocket` - For muzzle flash and shell ejection +- `ScopeSocket` - For optic attachments +- `GripSocket` - For foregrip attachments +- `BarrelSocket` - For heat effects and suppressors + +### Socket Configuration + +``` +Muzzle Socket Name: "MuzzleSocket" +Scope Socket Name: "ScopeSocket" +Grip Socket Name: "GripSocket" +Laser Socket Name: "LaserSocket" +``` + +--- + +## Ballistics & Performance + +Configure weapon stats for realistic performance: + +### Core Ballistics +``` +Bullet Properties: [Select EBBulletPropertiesAsset] +Damage: 42.0 (per shot) +Fire Rate: 600.0 (rounds per minute) +Muzzle Velocity: 71520.0 (cm/s, ~2350 fps) +Accuracy: 0.85 (0-1 scale, 1 = perfect) +Recoil Strength: 2.5 +Effective Range: 400.0 (meters) +``` + +### Magazine System +``` +Magazine Size: 30 +Max Reserve Ammo: 180 +Reload Time: 2.8 (seconds) +Tactical Reload Time: 2.2 (seconds, magazine not empty) +``` + +### Fire Modes +``` +Available Fire Modes: [Auto, Semiauto, Burst] +Default Fire Mode: Auto +Burst Count: 3 +``` + +--- + +## Advanced Niagara VFX Setup + +### Muzzle Effects + +**Primary Muzzle Flash:** +``` +Muzzle Flash: NS_MuzzleFlash_Rifle +Muzzle Flash Params: + - Intensity: 1.2 + - Scale: 1.0 + - Color Tint: (1.0, 0.9, 0.7, 1.0) +``` + +**Muzzle Smoke:** +``` +Muzzle Smoke: NS_MuzzleSmoke_Medium +Muzzle Smoke Params: + - Intensity: 0.8 + - Scale: 1.1 +``` + +**Suppressed Effects:** +``` +Suppressed Muzzle Flash: NS_MuzzleFlash_Suppressed +``` + +### Shell Ejection + +``` +Ejected Shell: NS_ShellEjection_762x39 +Shell Ejection Params: + - Intensity: 1.0 + - Scale: 1.0 +Shell Ejection Velocity: (250, 120, 80) +Shell Physics Material: PM_BrassShell +``` + +### Projectile Effects + +**Tracer System:** +``` +Tracer Effect: NS_Tracer_762x39 +Tracer Params: + - Intensity: 1.0 + - Color Tint: (1.0, 0.8, 0.3, 1.0) +``` + +**Bullet Trail:** +``` +Bullet Trail: NS_BulletWake_Rifle +Sonic Crack: NS_SonicCrack_Supersonic +``` + +### Impact Effects + +**Default Impact:** +``` +Hit Impact: NS_Impact_Default +Hit Impact Params: + - Intensity: 1.0 + - Scale: 1.0 +``` + +**Material-Specific Impacts:** +``` +Material Impact Effects: + - PM_Concrete: NS_Impact_Concrete + - PM_Metal: NS_Impact_Metal_Sparks + - PM_Wood: NS_Impact_Wood_Splinters + - PM_Flesh: NS_Impact_Blood +``` + +**Special Effects:** +``` +Penetration Effect: NS_Penetration_HighVel +Ricochet Effect: NS_Ricochet_Metal +``` + +### Heat System + +``` +Barrel Heat Effect: NS_BarrelHeat_Rifle +Enable Heat Distortion: ✓ +Enable Weather Effects: ✓ +Attachment Glow Effect: NS_WeaponGlow_Tactical +``` + +--- + +## Advanced Settings + +### Weapon Characteristics + +``` +Weapon Weight: 4.3 (kg, affects movement speed) +ADS Time: 0.35 (seconds to aim down sights) +Sprint To Fire Time: 0.28 (seconds from sprint to fire) +Equip Time: 0.9 (seconds to draw weapon) +``` + +### Special Features + +``` +Can Dual Wield: ✗ +Has Safety: ✓ +Suppressor Compatible: ✓ +``` + +### Niagara Advanced Features + +``` +Global Effect Params: + - Intensity: 1.0 + - Scale: 1.0 + - Color Tint: (1.0, 1.0, 1.0, 1.0) + +Use GPU Simulation: ✓ +Enable Effect LOD: ✓ +Max Heat Level: 100.0 +Heat Decay Rate: 12.0 (per second) +Heat Per Shot: 4.5 +``` + +--- + +## Audio Configuration + +### Sound Setup + +``` +Audio Config: + Fire Sound: SC_AK47_Fire + Empty Fire Sound: SC_DryFire_Rifle + Reload Sound: SC_AK47_Reload + Equip Sound: SC_WeaponDraw_Heavy + Unequip Sound: SC_WeaponHolster_Heavy +``` + +--- + +## Animation Configuration + +### First Person Animations + +``` +FP Fire: AS_FP_AK47_Fire +FP Reload: AS_FP_AK47_Reload +FP Equip: AS_FP_AK47_Equip +FP Idle: AS_FP_AK47_Idle +``` + +### Third Person Animations + +``` +TP Fire: AS_TP_Rifle_Fire +TP Reload: AS_TP_Rifle_Reload +TP Equip: AS_TP_Rifle_Equip +TP Idle: AS_TP_Rifle_Idle +``` + +### Animation Blueprint + +``` +Weapon Anim BP: ABP_WeaponBase +``` + +--- + +## Player Integration + +### Adding to Player Inventory + +**In Blueprint:** +```cpp +// Get the weapon controller component +WeaponController = GetComponentByClass(EBPlayerWeaponController) + +// Add weapon to inventory +WeaponController->AddWeapon(DA_AssaultRifle_AK47) + +// Switch to the weapon +WeaponController->SwitchToWeaponData(DA_AssaultRifle_AK47) +``` + +**In C++:** +```cpp +// Get weapon controller +UEBPlayerWeaponController* WeaponController = GetComponentByClass(); + +if (WeaponController && WeaponDataAsset) +{ + // Add to inventory + WeaponController->AddWeapon(WeaponDataAsset); + + // Equip weapon + WeaponController->SwitchToWeaponData(WeaponDataAsset); +} +``` + +### Default Weapon Setup + +Set the weapon as default in the **EBPlayerWeaponController**: + +``` +Default Weapon: DA_AssaultRifle_AK47 +Auto Equip On Begin Play: ✓ +``` + +--- + +## Testing Your Weapon + +### Basic Functionality Test + +1. **Add to player inventory** +2. **Test firing** - verify muzzle flash, shell ejection +3. **Test reloading** - check reload animations and timing +4. **Test weapon switching** - ensure smooth transitions +5. **Test ammo system** - verify magazine and reserve ammo + +### VFX Testing + +1. **Muzzle effects** - check flash intensity and smoke +2. **Shell ejection** - verify casing physics and bouncing +3. **Heat buildup** - fire continuously to test heat effects +4. **Impact effects** - shoot different materials +5. **Tracer effects** - verify bullet trail visibility + +### Performance Testing + +1. **Sustained fire** - test for frame rate drops +2. **Multiple weapons** - switch between different weapon types +3. **Network play** - test multiplayer synchronization +4. **Distance LOD** - verify effects scale with distance + +--- + +## Common Issues & Solutions + +### Muzzle Flash Not Appearing + +**Problem:** Niagara effect not triggering +**Solution:** +- Verify `MuzzleSocket` exists on weapon mesh +- Check Niagara system is assigned to `VFXConfig.MuzzleFlash` +- Ensure `bEnableAdvancedVFX` is true on player controller + +### Shell Ejection Wrong Direction + +**Problem:** Shells eject in wrong direction +**Solution:** +- Adjust `Shell Ejection Velocity` in weapon data +- Check weapon mesh socket orientation +- Verify shell ejection socket position + +### Heat Effects Not Working + +**Problem:** Heat effects don't show during sustained fire +**Solution:** +- Verify `BarrelHeatEffect` is assigned +- Check `Heat Per Shot` and `Max Heat Level` values +- Ensure heat threshold (>0.3) is reached + +### Performance Issues + +**Problem:** FPS drops with multiple effects +**Solution:** +- Enable `Use GPU Simulation` +- Enable `Enable Effect LOD` +- Reduce `Effect LOD Distance` for distant culling +- Optimize Niagara system complexity + +--- + +## Best Practices + +### Asset Organization + +``` +Content/ +├── Weapons/ +│ ├── Data/ +│ │ ├── Rifles/ +│ │ │ └── DA_AssaultRifle_AK47 +│ │ ├── Pistols/ +│ │ └── Shotguns/ +│ ├── Meshes/ +│ │ ├── FP/ +│ │ ├── TP/ +│ │ └── Static/ +│ ├── VFX/ +│ │ ├── MuzzleFlash/ +│ │ ├── Impacts/ +│ │ └── Tracers/ +│ └── Audio/ +``` + +### Performance Guidelines + +- **Use GPU simulation** for effects with >100 particles +- **Enable LOD** for all weapon effects +- **Limit concurrent effects** to ~5 per weapon +- **Pool Niagara systems** for frequently used effects +- **Test on target hardware** for performance validation + +### Visual Quality Tips + +- **Match muzzle flash** to weapon caliber and type +- **Scale effects** appropriately for weapon size +- **Use realistic colors** for tracers and impacts +- **Add weapon-specific variations** for immersion +- **Test in different lighting conditions** + +--- + +## Next Steps + +After setting up your weapon data asset: + +1. **Create weapon variants** (different attachments, camos) +2. **Set up attachment system** for scopes, grips, suppressors +3. **Configure weapon progressions** and unlock systems +4. **Add weapon-specific gameplay mechanics** +5. **Create comprehensive weapon testing scenarios** + +For advanced features like custom attachments and weapon modifications, see the [Advanced Weapon Systems Guide](advanced-weapon-systems.md). \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index 49d4a18..319a4c0 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -42,6 +42,7 @@ const sidebars = { type: 'category', label: 'Tutorials', items: [ + 'tutorials/weapon-data-setup', 'tutorials/gun-blueprint-guide', 'tutorials/blueprint-integration', 'tutorials/material-setup-guide', @@ -57,6 +58,7 @@ const sidebars = { 'advanced/editor-tools', 'advanced/editor-integration', 'advanced/json-import-export', + 'advanced/researcher-guide', 'advanced/migration-guide', ], }, @@ -67,6 +69,7 @@ const sidebars = { 'api/overview', 'api/bullet-reference', 'api/material-properties-reference', + 'api/parameter-validation-reference', 'api/debug-commands-reference', ], },