tengri/Source/TengriPlatformer/Character/TengriCharacter.cpp

343 lines
10 KiB
C++

// Request Games © All rights reserved
// Source/TengriPlatformer/Character/TengriCharacter.cpp
#include "TengriCharacter.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/ArrowComponent.h"
#include "TengriPlatformer/Movement/TengriMovementComponent.h"
#include "TengriPlatformer/World/TengriPickupActor.h"
#include "Kismet/KismetSystemLibrary.h"
// Enhanced Input
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
DEFINE_LOG_CATEGORY_STATIC(LogTengriCharacter, Log, All);
// ============================================================================
// CONSTANTS
// ============================================================================
namespace TengriCharacter
{
// Camera rotation speed multiplier in strafe mode
constexpr float StrafeRotationSpeedMultiplier = 2.0f;
}
// ============================================================================
// INITIALIZATION
// ============================================================================
ATengriCharacter::ATengriCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// Setup collision capsule
CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComponent"));
CapsuleComponent->InitCapsuleSize(34.0f, 88.0f);
CapsuleComponent->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
RootComponent = CapsuleComponent;
// Setup debug arrow
ArrowComponent = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
ArrowComponent->SetupAttachment(CapsuleComponent);
ArrowComponent->SetRelativeLocation(FVector(0.f, 0.f, 50.f));
// Setup character mesh
Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(CapsuleComponent);
Mesh->SetRelativeLocation(FVector(0.0f, 0.0f, -88.0f));
Mesh->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
// Setup custom movement component
MovementComponent = CreateDefaultSubobject<UTengriMovementComponent>(TEXT("TengriMovement"));
}
void ATengriCharacter::BeginPlay()
{
Super::BeginPlay();
}
// ============================================================================
// INPUT SETUP
// ============================================================================
void ATengriCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent);
if (!EnhancedInput)
{
UE_LOG(LogTengriCharacter, Error,
TEXT("SetupPlayerInputComponent: Enhanced Input not available"));
return;
}
// Bind Interact action (always active via IMC_Default)
if (InteractAction)
{
EnhancedInput->BindAction(InteractAction, ETriggerEvent::Started,
this, &ATengriCharacter::Interact);
}
// Bind Throw action (enabled via IMC_ItemHeld when item equipped)
if (ThrowAction)
{
EnhancedInput->BindAction(ThrowAction, ETriggerEvent::Started,
this, &ATengriCharacter::OnThrowInput);
}
// Bind Aim action (enabled via IMC_ItemHeld when item equipped)
if (AimAction)
{
EnhancedInput->BindAction(AimAction, ETriggerEvent::Started,
this, &ATengriCharacter::OnAimInput);
EnhancedInput->BindAction(AimAction, ETriggerEvent::Completed,
this, &ATengriCharacter::OnAimInput);
}
}
void ATengriCharacter::ToggleItemHeldContext(const bool bEnable) const
{
const APlayerController* PC = Cast<APlayerController>(GetController());
if (!PC)
{
UE_LOG(LogTengriCharacter, Warning,
TEXT("ToggleItemHeldContext: No player controller"));
return;
}
UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer());
if (!Subsystem)
{
UE_LOG(LogTengriCharacter, Error,
TEXT("ToggleItemHeldContext: Enhanced Input subsystem not available"));
return;
}
if (!ItemHeldMappingContext)
{
UE_LOG(LogTengriCharacter, Warning,
TEXT("ToggleItemHeldContext: ItemHeldMappingContext not assigned"));
return;
}
if (bEnable)
{
// Add item-equipped context (Priority 1 overrides default controls)
Subsystem->AddMappingContext(ItemHeldMappingContext, 1);
UE_LOG(LogTengriCharacter, Verbose, TEXT("Item context enabled"));
}
else
{
// Remove item context (revert to default movement controls)
Subsystem->RemoveMappingContext(ItemHeldMappingContext);
UE_LOG(LogTengriCharacter, Verbose, TEXT("Item context disabled"));
}
}
// ============================================================================
// INTERACTION LOGIC
// ============================================================================
void ATengriCharacter::Interact()
{
// SCENARIO 1: Holding item -> Drop gently
if (HeldItem)
{
const FVector GentleImpulse = GetActorForwardVector() * GentleDropSpeed;
// Apply gentle impulse (bVelChange = true ignores mass)
HeldItem->OnDropped(GentleImpulse, true);
HeldItem = nullptr;
// Disable combat mode controls
ToggleItemHeldContext(false);
return;
}
// SCENARIO 2: Empty hands -> Pick up nearest item
if (ATengriPickupActor* FoundItem = FindNearestPickup())
{
HeldItem = FoundItem;
HeldItem->OnPickedUp(Mesh, HandSocketName);
// Enable combat mode controls (Throw/Aim now active)
ToggleItemHeldContext(true);
UE_LOG(LogTengriCharacter, Verbose,
TEXT("Picked up item: %s"), *FoundItem->GetName());
}
}
void ATengriCharacter::OnThrowInput()
{
if (!HeldItem)
{
UE_LOG(LogTengriCharacter, Warning,
TEXT("OnThrowInput: No item held"));
return;
}
// Default to forward direction if raycasting fails
FVector ThrowDirection = GetActorForwardVector();
// ИСПРАВЛЕНИЕ: Переименовали Controller -> PC, чтобы избежать конфликта имен
AController* PC = GetController();
if (UWorld* World = GetWorld(); !PC || !World)
{
UE_LOG(LogTengriCharacter, Warning,
TEXT("OnThrowInput: Invalid controller or world context"));
// Continue with fallback direction
}
else
{
// ─────────────────────────────────────────────────────────────────
// PRECISE THROW TRAJECTORY CALCULATION
// ─────────────────────────────────────────────────────────────────
// Get camera position and rotation
FVector CameraLoc;
FRotator CameraRot;
PC->GetPlayerViewPoint(CameraLoc, CameraRot); // Используем PC вместо Controller
// Raycast from camera forward
const FVector TraceStart = CameraLoc;
const FVector TraceEnd = CameraLoc + (CameraRot.Vector() * ThrowTraceDistance);
FHitResult Hit;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
QueryParams.AddIgnoredActor(HeldItem);
// Find world target point (wall/enemy/floor where camera is looking)
const bool bHit = World->LineTraceSingleByChannel(
Hit,
TraceStart,
TraceEnd,
ECC_Visibility,
QueryParams
);
// Use hit point if found, otherwise use far endpoint
const FVector TargetPoint = bHit ? Hit.ImpactPoint : TraceEnd;
// Calculate throw direction FROM item TO target
const FVector HandLocation = HeldItem->GetActorLocation();
ThrowDirection = (TargetPoint - HandLocation).GetSafeNormal();
}
// Add arc elevation for natural parabolic trajectory
ThrowDirection += FVector(0.0f, 0.0f, ThrowArcElevation);
ThrowDirection.Normalize();
// Rotate character instantly to face throw direction
FRotator CharacterFaceRot = ThrowDirection.Rotation();
CharacterFaceRot.Pitch = 0.0f;
CharacterFaceRot.Roll = 0.0f;
if (MovementComponent)
{
MovementComponent->ForceRotation(CharacterFaceRot);
}
// Execute throw with calculated trajectory
const FVector Impulse = ThrowDirection * ThrowForce;
HeldItem->OnDropped(Impulse, true);
HeldItem = nullptr;
// Disable combat controls
ToggleItemHeldContext(false);
UE_LOG(LogTengriCharacter, Verbose,
TEXT("Threw item with force: %.1f cm/s"), ThrowForce);
// Note: bIsAiming state will be cleared by OnAimInput when player releases RMB/L2
}
void ATengriCharacter::OnAimInput(const FInputActionValue& Value)
{
// Extract boolean state (true = button pressed, false = released)
const bool bIsPressed = Value.Get<bool>();
// Skip if state hasn't changed (optimization)
if (bIsPressed == bIsAiming)
{
return;
}
bIsAiming = bIsPressed;
// Sync strafe mode with movement component
if (MovementComponent)
{
MovementComponent->SetStrafing(bIsAiming);
UE_LOG(LogTengriCharacter, Verbose,
TEXT("Aim mode: %s"), bIsAiming ? TEXT("ON") : TEXT("OFF"));
}
}
// ============================================================================
// PICKUP DETECTION
// ============================================================================
ATengriPickupActor* ATengriCharacter::FindNearestPickup() const
{
UWorld* World = GetWorld();
if (!World)
{
return nullptr;
}
const FVector SpherePos = GetActorLocation();
// Define object types to detect (physics objects and dynamic actors)
TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_PhysicsBody));
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_WorldDynamic));
// Ignore self in overlap query
TArray<AActor*> IgnoreActors;
IgnoreActors.Add(const_cast<ATengriCharacter*>(this));
// Find all overlapping pickups
TArray<AActor*> OverlappingActors;
UKismetSystemLibrary::SphereOverlapActors(
World,
SpherePos,
PickupRadius,
ObjectTypes,
ATengriPickupActor::StaticClass(),
IgnoreActors,
OverlappingActors
);
// Find nearest un-held pickup
ATengriPickupActor* NearestItem = nullptr;
float MinDistSq = FMath::Square(PickupRadius);
for (AActor* Actor : OverlappingActors)
{
ATengriPickupActor* Item = Cast<ATengriPickupActor>(Actor);
if (!Item || Item->IsHeld())
{
continue;
}
const float DistSq = FVector::DistSquared(SpherePos, Item->GetActorLocation());
if (DistSq < MinDistSq)
{
MinDistSq = DistSq;
NearestItem = Item;
}
}
return NearestItem;
}