// 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(TEXT("CapsuleComponent")); CapsuleComponent->InitCapsuleSize(34.0f, 88.0f); CapsuleComponent->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName); RootComponent = CapsuleComponent; // Setup debug arrow ArrowComponent = CreateDefaultSubobject(TEXT("Arrow")); ArrowComponent->SetupAttachment(CapsuleComponent); ArrowComponent->SetRelativeLocation(FVector(0.f, 0.f, 50.f)); // Setup character mesh Mesh = CreateDefaultSubobject(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(TEXT("TengriMovement")); } void ATengriCharacter::BeginPlay() { Super::BeginPlay(); } // ============================================================================ // INPUT SETUP // ============================================================================ void ATengriCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); UEnhancedInputComponent* EnhancedInput = Cast(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(GetController()); if (!PC) { UE_LOG(LogTengriCharacter, Warning, TEXT("ToggleItemHeldContext: No player controller")); return; } UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(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(); // 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> ObjectTypes; ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_PhysicsBody)); ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_WorldDynamic)); // Ignore self in overlap query TArray IgnoreActors; IgnoreActors.Add(const_cast(this)); // Find all overlapping pickups TArray 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(Actor); if (!Item || Item->IsHeld()) { continue; } const float DistSq = FVector::DistSquared(SpherePos, Item->GetActorLocation()); if (DistSq < MinDistSq) { MinDistSq = DistSq; NearestItem = Item; } } return NearestItem; }