diff --git a/Content/BasicShapes/CustomDefaultSkeletalMesh.ts b/Content/BasicShapes/CustomDefaultSkeletalMesh.ts new file mode 100644 index 0000000..14149f6 --- /dev/null +++ b/Content/BasicShapes/CustomDefaultSkeletalMesh.ts @@ -0,0 +1,5 @@ +// Content/BasicShapes/CustomDefaultSkeletalMesh.ts + +import { SkeletalMesh } from '/Content/UE/SkeletalMesh.ts'; + +export class CustomDefaultSkeletalMesh extends SkeletalMesh {} diff --git a/Content/BasicShapes/CustomDefaultSkeletalMesh.uasset b/Content/BasicShapes/CustomDefaultSkeletalMesh.uasset new file mode 100644 index 0000000..0384d4a --- /dev/null +++ b/Content/BasicShapes/CustomDefaultSkeletalMesh.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0519fdf8a1c138dcdcde8cbec5a8637efac5c3f3cfce37e1fbcdffadf7049d23 +size 19429 diff --git a/Content/Blueprints/BP_MainCharacter.ts b/Content/Blueprints/BP_MainCharacter.ts index aea45cd..d0e283b 100644 --- a/Content/Blueprints/BP_MainCharacter.ts +++ b/Content/Blueprints/BP_MainCharacter.ts @@ -5,26 +5,34 @@ import { AC_DebugHUD } from '/Content/Debug/Components/AC_DebugHUD.ts'; import { AC_InputDevice } from '/Content/Input/Components/AC_InputDevice.ts'; import { IMC_Default } from '/Content/Input/IMC_Default.ts'; import { AC_ToastSystem } from '/Content/Toasts/Components/AC_ToastSystem.ts'; -import { CapsuleComponent } from '/Content/UE/CapsuleComponent.ts'; import { Cast } from '/Content/UE/Cast.ts'; import type { Controller } from '/Content/UE/Controller.ts'; import { EnhancedInputLocalPlayerSubsystem } from '/Content/UE/EnhancedInputLocalPlayerSubsystem.ts'; import type { Float } from '/Content/UE/Float.ts'; import { MathLibrary } from '/Content/UE/MathLibrary.ts'; -import { Pawn } from '/Content/UE/Pawn.ts'; import type { PlayerController } from '/Content/UE/PlayerController.ts'; import { Rotator } from '/Content/UE/Rotator.ts'; import { SystemLibrary } from '/Content/UE/SystemLibrary.ts'; import { Vector } from '/Content/UE/Vector.ts'; import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts'; import { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts'; +import { TengriCharacter } from '/Source/TengriPlatformer/Character/TengriCharacter.ts'; +import { IA_Interact } from '/Content/Input/Actions/IA_Inreract.ts'; +import { IA_Throw } from '/Content/Input/Actions/IA_Throw.ts'; +import { IA_Aim } from '/Content/Input/Actions/IA_Aim.ts'; +import { IMC_ItemHeld } from '/Content/Input/IMC_ItemHeld.ts'; +import { CreateWidget } from '/Content/UE/CteateWidget.ts'; +import { WBP_HUD } from '/Content/UI/WBP_HUD.ts'; +import { SpringArmComponent } from '/Content/UE/SpringArmComponent.ts'; +import { LinearColor } from '/Content/UE/LinearColor.ts'; +import { CustomDefaultSkeletalMesh } from '/Content/BasicShapes/CustomDefaultSkeletalMesh.ts'; /** * Main Character Blueprint * Core player character with deterministic movement system * Integrates debug HUD and toast notification systems */ -export class BP_MainCharacter extends Pawn { +export class BP_MainCharacter extends TengriCharacter { // ════════════════════════════════════════════════════════════════════════════════════════ // GRAPHS // ════════════════════════════════════════════════════════════════════════════════════════ @@ -145,6 +153,32 @@ export class BP_MainCharacter extends Pawn { this.TengriMovement.SetInputVector(new Vector(0, 0, 0)); } + EnhancedInputActionJumpTriggered(): void { + this.MovementComponent.SetJumpInput(true); + } + + EnhancedInputActionJumpCompleted(): void { + this.MovementComponent.SetJumpInput(false); + } + + MovementComponentOnLanded(IsHeavy: boolean): void { + if (IsHeavy) { + SystemLibrary.PrintString( + 'Boom! (Heavy)', + true, + true, + new LinearColor(1, 0, 0, 1) + ); + } else { + SystemLibrary.PrintString( + 'Tap (Light)', + true, + true, + new LinearColor(0, 1, 0, 1) + ); + } + } + /** * Initialize all systems when character spawns * Order: Toast → Debug → Movement (movement last as it may generate debug output) @@ -170,6 +204,8 @@ export class BP_MainCharacter extends Pawn { this.InputDeviceComponent, this.DebugHUDComponent ); + + CreateWidget(WBP_HUD).AddToViewport(); } /** @@ -198,12 +234,32 @@ export class BP_MainCharacter extends Pawn { this.InputDeviceComponent.UpdateDebugPage(); this.CameraComponent.UpdateDebugPage(); } + + this.SpringArm.TargetArmLength = MathLibrary.FInterpTo( + this.SpringArm.TargetArmLength, + this.bIsAiming ? this.AimArmLength : this.DefaultArmLength, + DeltaTime, + 10.0 + ); + + this.SpringArm.TargetOffset = MathLibrary.VInterpTo( + this.SpringArm.TargetOffset, + this.bIsAiming ? this.AimSocketOffset : this.DefaultSocketOffset, + DeltaTime, + 10.0 + ); } // ════════════════════════════════════════════════════════════════════════════════════════ // VARIABLES // ════════════════════════════════════════════════════════════════════════════════════════ + /** + * Camera system component - handles camera rotation and sensitivity + * @category Components + */ + SpringArm = new SpringArmComponent(); + /** * Camera system component - handles camera rotation and sensitivity * @category Components @@ -222,7 +278,9 @@ export class BP_MainCharacter extends Pawn { */ ToastSystemComponent = new AC_ToastSystem(); - TengriMovement = new TengriMovementComponent(DA_TengriMovementConfig); + TengriMovement = new TengriMovementComponent({ + MovementConfig: DA_TengriMovementConfig, + }); /** * Debug HUD system - displays movement parameters and performance metrics @@ -230,12 +288,6 @@ export class BP_MainCharacter extends Pawn { */ DebugHUDComponent = new AC_DebugHUD(); - /** - * Character's capsule component for collision detection - * @category Components - */ - CharacterCapsule = new CapsuleComponent(); - /** * Master debug toggle - controls all debug systems (HUD, toasts, visual debug) * @category Debug @@ -243,8 +295,43 @@ export class BP_MainCharacter extends Pawn { */ private ShowDebugInfo: boolean = true; + /** + * @category Camera + * @instanceEditable true + */ + private readonly DefaultArmLength: Float = 400.0; + + /** + * @category Camera + * @instanceEditable true + */ + private readonly AimArmLength: Float = 250.0; + + /** + * @category Camera + * @instanceEditable true + */ + private readonly DefaultSocketOffset: Vector = new Vector(0.0, 0.0, 0.0); + + /** + * @category Camera + * @instanceEditable true + */ + private readonly AimSocketOffset: Vector = new Vector(0.0, 100.0, 60.0); + /** * Cached delta time from last tick - used for time-based calculations */ private DeltaTime: Float = 0.0; + + constructor() { + super(); + + this.InteractAction = IA_Interact; + this.ThrowAction = IA_Throw; + this.AimAction = IA_Aim; + this.ItemHeldMappingContext = IMC_ItemHeld; + + this.Mesh = new CustomDefaultSkeletalMesh(); + } } diff --git a/Content/Blueprints/BP_MainCharacter.uasset b/Content/Blueprints/BP_MainCharacter.uasset index 0c78bce..78e498c 100644 --- a/Content/Blueprints/BP_MainCharacter.uasset +++ b/Content/Blueprints/BP_MainCharacter.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59bc47bf1be763d5f4e13c709fc3ee6d89c3fd33dfb9024383f696f3273a32a1 -size 347043 +oid sha256:cc6a7a86960a45d00c942deee974df62de7897a9432ec37d4db8e34996cf59a1 +size 413601 diff --git a/Content/Blueprints/BP_Rock.ts b/Content/Blueprints/BP_Rock.ts new file mode 100644 index 0000000..0d69272 --- /dev/null +++ b/Content/Blueprints/BP_Rock.ts @@ -0,0 +1,5 @@ +// Content/Blueprints/BP_Rock.ts + +import { TengriPickupActor } from '/Source/TengriPlatformer/World/TengriPickupActor.ts'; + +export class BP_Rock extends TengriPickupActor {} diff --git a/Content/Blueprints/BP_Rock.uasset b/Content/Blueprints/BP_Rock.uasset new file mode 100644 index 0000000..6ff1597 --- /dev/null +++ b/Content/Blueprints/BP_Rock.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:862c0746068576680cfea3db432d9ae165667381ad9ed367b67f610c02c13476 +size 33241 diff --git a/Content/Input/Actions/IA_Aim.ts b/Content/Input/Actions/IA_Aim.ts new file mode 100644 index 0000000..68c5196 --- /dev/null +++ b/Content/Input/Actions/IA_Aim.ts @@ -0,0 +1,6 @@ +// Content/Input/Actions/IA_Aim + +import { InputAction } from '/Content/UE/InputAction.ts'; +import { Name } from '/Content/UE/Name.ts'; + +export const IA_Aim = new InputAction(null, new Name('IA_Aim')); diff --git a/Content/Input/Actions/IA_Aim.uasset b/Content/Input/Actions/IA_Aim.uasset new file mode 100644 index 0000000..e468fdc --- /dev/null +++ b/Content/Input/Actions/IA_Aim.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:114e8afd114d2a4a1dc742d6f61e8fd113c980b7f22850b3647bdb1523685f57 +size 1137 diff --git a/Content/Input/Actions/IA_Inreract.ts b/Content/Input/Actions/IA_Inreract.ts new file mode 100644 index 0000000..0795ae7 --- /dev/null +++ b/Content/Input/Actions/IA_Inreract.ts @@ -0,0 +1,6 @@ +// Content/Input/Actions/IA_Interact + +import { InputAction } from '/Content/UE/InputAction.ts'; +import { Name } from '/Content/UE/Name.ts'; + +export const IA_Interact = new InputAction(null, new Name('IA_Interact')); diff --git a/Content/Input/Actions/IA_Interact.uasset b/Content/Input/Actions/IA_Interact.uasset new file mode 100644 index 0000000..4c94e2b --- /dev/null +++ b/Content/Input/Actions/IA_Interact.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3362699676a14d191def735d7bfda69f2dfaa2530c378ec162d9deb836de8f73 +size 1162 diff --git a/Content/Input/Actions/IA_Throw.ts b/Content/Input/Actions/IA_Throw.ts new file mode 100644 index 0000000..e6eb556 --- /dev/null +++ b/Content/Input/Actions/IA_Throw.ts @@ -0,0 +1,5 @@ +// Content/Input/Actions/IA_Aim + +import { InputAction } from '/Content/UE/InputAction.ts'; +import { Name } from '/Content/UE/Name.ts'; +export const IA_Throw = new InputAction(null, new Name('IA_Aim')); diff --git a/Content/Input/Actions/IA_Throw.uasset b/Content/Input/Actions/IA_Throw.uasset new file mode 100644 index 0000000..7a46b34 --- /dev/null +++ b/Content/Input/Actions/IA_Throw.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c287146cedd5f9a2730d6bfd82275cc812c18ce8de5b8c07a9857deed249503 +size 1147 diff --git a/Content/Input/IMC_Default.ts b/Content/Input/IMC_Default.ts index 94c9681..5bfbdf0 100644 --- a/Content/Input/IMC_Default.ts +++ b/Content/Input/IMC_Default.ts @@ -11,6 +11,7 @@ import { IA_ToggleVisualDebug } from '/Content/Input/Actions/IA_ToggleVisualDebu import { InputMappingContext } from '/Content/UE/InputMappingContext.ts'; import { Key } from '/Content/UE/Key.ts'; import { IA_Jump } from '/Content/Input/Actions/IA_Jump.ts'; +import { IA_Interact } from '/Content/Input/Actions/IA_Inreract.ts'; export const IMC_Default = new InputMappingContext(); @@ -23,3 +24,4 @@ IMC_Default.mapKey(IA_ToggleVisualDebug, new Key('IA_ToggleVisualDebug')); IMC_Default.mapKey(IA_Look, new Key('IA_Look')); IMC_Default.mapKey(IA_Move, new Key('IA_Move')); IMC_Default.mapKey(IA_Jump, new Key('IA_Jump')); +IMC_Default.mapKey(IA_Interact, new Key('IA_Interact')); diff --git a/Content/Input/IMC_Default.uasset b/Content/Input/IMC_Default.uasset index c8d6338..92b20d1 100644 --- a/Content/Input/IMC_Default.uasset +++ b/Content/Input/IMC_Default.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93af5cf62d01121d470ee4154513ac5d41d8658ccdb6477cf7f90c04ca6f68d0 -size 12206 +oid sha256:e75c195781277428da60aa7b71d50bb05e661c8c01e85daab2a0b8f13f24be32 +size 13002 diff --git a/Content/Input/IMC_ItemHeld.ts b/Content/Input/IMC_ItemHeld.ts new file mode 100644 index 0000000..77aa31f --- /dev/null +++ b/Content/Input/IMC_ItemHeld.ts @@ -0,0 +1,11 @@ +// Content/Input/IMC_ItemHeld.ts + +import { InputMappingContext } from '/Content/UE/InputMappingContext.ts'; +import { Key } from '/Content/UE/Key.ts'; +import { IA_Aim } from '/Content/Input/Actions/IA_Aim.ts'; +import { IA_Throw } from '/Content/Input/Actions/IA_Throw.ts'; + +export const IMC_ItemHeld = new InputMappingContext(); + +IMC_ItemHeld.mapKey(IA_Aim, new Key('IA_Aim')); +IMC_ItemHeld.mapKey(IA_Throw, new Key('IA_Throw')); diff --git a/Content/Input/IMC_ItemHeld.uasset b/Content/Input/IMC_ItemHeld.uasset new file mode 100644 index 0000000..df9abdd --- /dev/null +++ b/Content/Input/IMC_ItemHeld.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92fc5f128bca6cb97e9adfa978cde6a7cff4920d51ca5a977fb464d2f669d035 +size 3304 diff --git a/Content/Levels/TestLevel.ts b/Content/Levels/TestLevel.ts index 464aa05..c38de6b 100644 --- a/Content/Levels/TestLevel.ts +++ b/Content/Levels/TestLevel.ts @@ -1,5 +1,7 @@ // Content/Levels/TestLevel.ts import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts'; +import { BP_Rock } from '/Content/Blueprints/BP_Rock.ts'; new BP_MainCharacter(); +new BP_Rock(); diff --git a/Content/Levels/TestLevel.umap b/Content/Levels/TestLevel.umap index 8ac2e4c..2de23f8 100644 --- a/Content/Levels/TestLevel.umap +++ b/Content/Levels/TestLevel.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:050c1016d489a850f60541762f9fb12a9e35e46b0c54f780570364c3d4d47250 -size 573337 +oid sha256:56542633859c7aa5592d39c7af3bb0dc45f3de4c712401410b24d1f38c059927 +size 575376 diff --git a/Content/UE/ArrowComponent.ts b/Content/UE/ArrowComponent.ts new file mode 100644 index 0000000..0d2e4a4 --- /dev/null +++ b/Content/UE/ArrowComponent.ts @@ -0,0 +1,10 @@ +// Content/UE/ArrowComponent.ts + +import { Name } from '/Content/UE/Name.ts'; +import { UEObject } from '/Content/UE/UEObject.ts'; + +export class ArrowComponent extends UEObject { + constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) { + super(outer, name); + } +} diff --git a/Content/UE/MathLibrary.ts b/Content/UE/MathLibrary.ts index dc88f66..aadf960 100644 --- a/Content/UE/MathLibrary.ts +++ b/Content/UE/MathLibrary.ts @@ -114,6 +114,50 @@ class MathLibraryClass extends BlueprintFunctionLibrary { return Current + DeltaMove; } + /** + * Interpolate a vector value towards a target + * @param Current - Current value + * @param Target - Target value + * @param DeltaTime - Time since last update + * @param InterpSpeed - Speed of interpolation + * @returns New interpolated value + * @example + * // Interpolate Vector(0,0,0) towards Vector(10,10,0) over 1 second at speed 5 + * VInterpTo(new Vector(0,0,0), new Vector(10,10,0), 1, 5) // returns Vector(5,5,0) + */ + public VInterpTo( + Current: Vector, + Target: Vector, + DeltaTime: Float, + InterpSpeed: Float + ): Vector { + if (InterpSpeed <= 0) { + return Target; + } + const Dist = new Vector( + Target.X - Current.X, + Target.Y - Current.Y, + Target.Z - Current.Z + ); + if ( + this.VectorLength( + new Vector(Dist.X * Dist.X, Dist.Y * Dist.Y, Dist.Z * Dist.Z) + ) < 0.00001 + ) { + return Target; + } + const DeltaMove = new Vector( + Dist.X * Math.min(DeltaTime * InterpSpeed, 1), + Dist.Y * Math.min(DeltaTime * InterpSpeed, 1), + Dist.Z * Math.min(DeltaTime * InterpSpeed, 1) + ); + return new Vector( + Current.X + DeltaMove.X, + Current.Y + DeltaMove.Y, + Current.Z + DeltaMove.Z + ); + } + /** * Get right vector from roll, pitch, yaw angles * @param Roll - Rotation around forward axis in radians diff --git a/Content/UE/SkeletalMesh.ts b/Content/UE/SkeletalMesh.ts new file mode 100644 index 0000000..4e458e9 --- /dev/null +++ b/Content/UE/SkeletalMesh.ts @@ -0,0 +1,11 @@ +// Content/UE/SkeletalMesh.ts + +import { SkinnedAsset } from '/Content/UE/SkinnedAsset.ts'; +import { UEObject } from '/Content/UE/UEObject.ts'; +import { Name } from '/Content/UE/Name.ts'; + +export class SkeletalMesh extends SkinnedAsset { + constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) { + super(outer, name); + } +} diff --git a/Content/UE/SkinnedAsset.ts b/Content/UE/SkinnedAsset.ts new file mode 100644 index 0000000..4ccfbac --- /dev/null +++ b/Content/UE/SkinnedAsset.ts @@ -0,0 +1,11 @@ +// Content/UE/SkinnedAsset.ts + +import { StreamableRenderAsset } from '/Content/UE/StreamableRenderAsset.ts'; +import { UEObject } from '/Content/UE/UEObject.ts'; +import { Name } from '/Content/UE/Name.ts'; + +export class SkinnedAsset extends StreamableRenderAsset { + constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) { + super(outer, name); + } +} diff --git a/Content/UE/SpringArmComponent.ts b/Content/UE/SpringArmComponent.ts new file mode 100644 index 0000000..2474625 --- /dev/null +++ b/Content/UE/SpringArmComponent.ts @@ -0,0 +1,18 @@ +// Content/UE/SpringArmComponent.ts + +import { SceneComponent } from '/Content/UE/SceneComponent.ts'; +import type { UEObject } from '/Content/UE/UEObject.ts'; +import type { Float } from '/Content/UE/Float.ts'; +import { Vector } from '/Content/UE/Vector.ts'; + +export class SpringArmComponent extends SceneComponent { + constructor(outer: UEObject | null = null, name: string = 'None') { + super(outer, name); + + this.TargetArmLength = 0.0; + this.TargetOffset = new Vector(0.0, 0.0, 0.0); + } + + public TargetArmLength: Float; + public TargetOffset: Vector; +} diff --git a/Content/UE/StreamableRenderAsset.ts b/Content/UE/StreamableRenderAsset.ts new file mode 100644 index 0000000..4b656b0 --- /dev/null +++ b/Content/UE/StreamableRenderAsset.ts @@ -0,0 +1,10 @@ +// Content/UE/StreamableRenderAsset.ts + +import { Name } from '/Content/UE/Name.ts'; +import { UEObject } from '/Content/UE/UEObject.ts'; + +export class StreamableRenderAsset extends UEObject { + constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) { + super(outer, name); + } +} diff --git a/Content/UI/WBP_HUD.ts b/Content/UI/WBP_HUD.ts new file mode 100644 index 0000000..be855b3 --- /dev/null +++ b/Content/UI/WBP_HUD.ts @@ -0,0 +1,15 @@ +// Content/UI/WBP_HUD.ts + +import { UserWidget } from '/Content/UE/UserWidget.ts'; +import { BP_MainCharacter } from '/Content/Blueprints/BP_MainCharacter.ts'; +import { ESlateVisibility } from '/Content/UE/ESlateVisibility.ts'; + +export class WBP_HUD extends UserWidget { + private ChangeVisibility(): ESlateVisibility { + const player = new BP_MainCharacter(); + + return player.bIsAiming + ? ESlateVisibility.Visible + : ESlateVisibility.Hidden; + } +} diff --git a/Content/UI/WBP_HUD.uasset b/Content/UI/WBP_HUD.uasset new file mode 100644 index 0000000..c44e9e1 --- /dev/null +++ b/Content/UI/WBP_HUD.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:191163f74607876a64a997806785757906501412910c9e94bc497061ac66c219 +size 48028 diff --git a/Source/TengriPlatformer/Character/TengriCharacter.cpp b/Source/TengriPlatformer/Character/TengriCharacter.cpp new file mode 100644 index 0000000..b27f15b --- /dev/null +++ b/Source/TengriPlatformer/Character/TengriCharacter.cpp @@ -0,0 +1,343 @@ +// 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; +} \ No newline at end of file diff --git a/Source/TengriPlatformer/Character/TengriCharacter.h b/Source/TengriPlatformer/Character/TengriCharacter.h new file mode 100644 index 0000000..9fe028d --- /dev/null +++ b/Source/TengriPlatformer/Character/TengriCharacter.h @@ -0,0 +1,161 @@ +// Request Games © All rights reserved + +// Source/TengriPlatformer/Character/TengriCharacter.h + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Pawn.h" +#include "InputActionValue.h" +#include "TengriCharacter.generated.h" + +// Forward declarations +class UCapsuleComponent; +class USkeletalMeshComponent; +class UTengriMovementComponent; +class ATengriPickupActor; +class UArrowComponent; +class UInputMappingContext; +class UInputAction; + +/** + * Main player character class with item interaction and throwing mechanics. + * Supports dynamic input context switching for item-based actions. + */ +UCLASS() +class TENGRIPLATFORMER_API ATengriCharacter : public APawn +{ + GENERATED_BODY() + +public: + ATengriCharacter(); + +protected: + virtual void BeginPlay() override; + virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; + +public: + // ======================================================================== + // COMPONENTS + // ======================================================================== + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") + TObjectPtr CapsuleComponent; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") + TObjectPtr Mesh; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") + TObjectPtr ArrowComponent; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") + TObjectPtr MovementComponent; + + // ======================================================================== + // INPUT CONFIG + // ======================================================================== + + /** Interact action (E / Square) - Always available */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") + TObjectPtr InteractAction; + + /** Throw action (LMB / R2) - Active only in ItemHeld context */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") + TObjectPtr ThrowAction; + + /** Aim action (RMB / L2) - Active only in ItemHeld context */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") + TObjectPtr AimAction; + + /** Input mapping context for item-equipped state (Priority 1) */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") + TObjectPtr ItemHeldMappingContext; + + // ======================================================================== + // INTERACTION API + // ======================================================================== + + /** + * Handle interact button (E/Square). + * Behavior: + * - If holding item: Drop gently with small forward impulse + * - If empty hands: Pick up nearest item in radius + */ + UFUNCTION(BlueprintCallable, Category = "Interaction") + void Interact(); + + /** + * Execute throw action (LMB/R2). + * Uses camera-based raycasting for precise trajectory calculation. + * Automatically rotates character to face throw direction. + * @note Only callable when HeldItem is valid + */ + void OnThrowInput(); + + /** + * Handle aim input state changes (RMB/L2). + * Toggles strafe mode on MovementComponent for camera-aligned rotation. + * @param Value - Input action value (true = pressed, false = released) + */ + void OnAimInput(const FInputActionValue& Value); + + /** Check if character is currently holding an item */ + UFUNCTION(BlueprintPure, Category = "Interaction") + bool HasItem() const { return HeldItem != nullptr; } + +protected: + // ======================================================================== + // INTERACTION STATE + // ======================================================================== + + /** Currently held item reference (nullptr if empty-handed) */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction|State") + TObjectPtr HeldItem; + + /** Aiming state flag (affects camera and animation) */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction|State") + bool bIsAiming = false; + + // ======================================================================== + // INTERACTION CONFIG + // ======================================================================== + + /** Socket name on character mesh for attaching held items */ + UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config") + FName HandSocketName = FName("HandSocket"); + + /** Sphere radius for detecting nearby pickups (cm) */ + UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config") + float PickupRadius = 150.0f; + + /** Throw velocity magnitude (cm/s) */ + UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config") + float ThrowForce = 1200.0f; + + /** Gentle drop impulse for interact-to-drop (cm/s) */ + UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config") + float GentleDropSpeed = 50.0f; + + /** Raycast distance for throw targeting (cm) */ + UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config") + float ThrowTraceDistance = 5000.0f; + + /** Arc elevation adjustment for throw trajectory */ + UPROPERTY(EditDefaultsOnly, Category = "Interaction|Config") + float ThrowArcElevation = 0.15f; + +private: + /** + * Find nearest interactable pickup within radius. + * Uses sphere overlap against PhysicsBody and WorldDynamic channels. + * @return Nearest valid pickup, or nullptr if none found + */ + ATengriPickupActor* FindNearestPickup() const; + + /** + * Toggle ItemHeld input mapping context on/off. + * Manages context priority to enable/disable throw and aim actions. + * @param bEnable - True to add context, false to remove + */ + void ToggleItemHeldContext(bool bEnable) const; +}; \ No newline at end of file diff --git a/Source/TengriPlatformer/Character/TengriCharacter.ts b/Source/TengriPlatformer/Character/TengriCharacter.ts new file mode 100644 index 0000000..fd60ab0 --- /dev/null +++ b/Source/TengriPlatformer/Character/TengriCharacter.ts @@ -0,0 +1,55 @@ +// Source/TengriPlatformer/Character/TengriCharacter.ts + +import { Pawn } from '/Content/UE/Pawn.ts'; +import { CapsuleComponent } from '/Content/UE/CapsuleComponent.ts'; +import { SkeletalMesh } from '/Content/UE/SkeletalMesh.ts'; +import { ArrowComponent } from '/Content/UE/ArrowComponent.ts'; +import { TengriMovementComponent } from '/Source/TengriPlatformer/Movement/TengriMovementComponent.ts'; +import { InputAction } from '/Content/UE/InputAction.ts'; +import { InputMappingContext } from '/Content/UE/InputMappingContext.ts'; + +export class TengriCharacter extends Pawn { + constructor() { + super(); + + this.CapsuleComponent = new CapsuleComponent(); + this.Mesh = new SkeletalMesh(); + this.ArrowComponent = new ArrowComponent(); + this.MovementComponent = new TengriMovementComponent(); + + this.InteractAction = new InputAction(); + this.ThrowAction = new InputAction(); + this.AimAction = new InputAction(); + this.ItemHeldMappingContext = new InputMappingContext(); + } + + // ======================================================================== + // COMPONENTS + // ======================================================================== + + public CapsuleComponent: CapsuleComponent; + public Mesh: SkeletalMesh; + public ArrowComponent: ArrowComponent; + public MovementComponent: TengriMovementComponent; + + // ======================================================================== + // INPUT CONFIG + // ======================================================================== + + public InteractAction: InputAction; + public ThrowAction: InputAction; + public AimAction: InputAction; + public ItemHeldMappingContext: InputMappingContext; + + // ======================================================================== + // INTERACTION API + // ======================================================================== + + public Interact(): void {} + public OnItemHeldInput(): void {} + + // ======================================================================== + // INTERACTION STATE + // ======================================================================== + public bIsAiming: boolean = false; +} diff --git a/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.cpp b/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.cpp index 29115a7..e61a44c 100644 --- a/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.cpp +++ b/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.cpp @@ -48,8 +48,7 @@ FTengriSweepResult UTengriCollisionResolver::PerformSweep( const UObject* WorldContext, const FVector& Start, const FVector& End, - const UCapsuleComponent* Capsule, - bool bShowDebug) + const UCapsuleComponent* Capsule) { FTengriSweepResult Result; Result.Location = End; @@ -60,7 +59,7 @@ FTengriSweepResult UTengriCollisionResolver::PerformSweep( return Result; } - UWorld* World = WorldContext->GetWorld(); + const UWorld* World = WorldContext->GetWorld(); if (!World) { return Result; @@ -118,7 +117,7 @@ bool UTengriCollisionResolver::StepUp( const FVector& DesiredDelta, const FHitResult& ImpactHit, const UCapsuleComponent* Capsule, - float MaxStepHeight, + const float MaxStepHeight, FVector& OutLocation) { // Reject sloped surfaces and overhangs @@ -136,7 +135,7 @@ bool UTengriCollisionResolver::StepUp( // === Phase A: Trace Up === const FVector StepUpEnd = StartLocation + FVector(0.f, 0.f, MaxStepHeight); - const FTengriSweepResult UpSweep = PerformSweep(WorldContext, StartLocation, StepUpEnd, Capsule, false); + const FTengriSweepResult UpSweep = PerformSweep(WorldContext, StartLocation, StepUpEnd, Capsule); // Reject if not enough headroom (hit ceiling before half step height) if (UpSweep.bBlocked && UpSweep.Location.Z < (StartLocation.Z + MaxStepHeight * 0.5f)) @@ -157,7 +156,7 @@ bool UTengriCollisionResolver::StepUp( const float CheckDist = FMath::Max(DesiredDelta.Size2D(), MinCheckDist); const FVector ForwardEnd = UpSweep.Location + (ForwardDir * CheckDist); - const FTengriSweepResult ForwardSweep = PerformSweep(WorldContext, UpSweep.Location, ForwardEnd, Capsule, false); + const FTengriSweepResult ForwardSweep = PerformSweep(WorldContext, UpSweep.Location, ForwardEnd, Capsule); // Reject if obstacle continues upward (wall, next stair step) if (ForwardSweep.bBlocked) @@ -168,7 +167,7 @@ bool UTengriCollisionResolver::StepUp( // === Phase C: Trace Down === const FVector DownStart = ForwardSweep.Location; const FVector DownEnd = DownStart - FVector(0.f, 0.f, MaxStepHeight * TengriPhysics::DownTraceMultiplier); - const FTengriSweepResult DownSweep = PerformSweep(WorldContext, DownStart, DownEnd, Capsule, false); + const FTengriSweepResult DownSweep = PerformSweep(WorldContext, DownStart, DownEnd, Capsule); if (!DownSweep.bBlocked || DownSweep.Hit.ImpactNormal.Z < TengriPhysics::MinGroundNormalZ) { @@ -227,7 +226,7 @@ bool UTengriCollisionResolver::SnapToGround( const FVector Start = Capsule->GetComponentLocation(); const FVector End = Start - FVector(0.f, 0.f, SnapDistance); - if (const FTengriSweepResult Sweep = PerformSweep(WorldContext, Start, End, Capsule, false); Sweep.bBlocked && Thresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z)) + if (const FTengriSweepResult Sweep = PerformSweep(WorldContext, Start, End, Capsule); Sweep.bBlocked && Thresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z)) { OutLocation = Sweep.Location; OutHit = Sweep.Hit; @@ -278,9 +277,7 @@ namespace // If wall would push us up, force horizontal movement only if (ClipDelta.Z > 0.f) { - FVector HorizontalTangent = FVector::CrossProduct(ImpactNormal, FVector(0.f, 0.f, 1.f)).GetSafeNormal(); - - if (!HorizontalTangent.IsNearlyZero()) + if (FVector HorizontalTangent = FVector::CrossProduct(ImpactNormal, FVector(0.f, 0.f, 1.f)).GetSafeNormal(); !HorizontalTangent.IsNearlyZero()) { if (FVector::DotProduct(HorizontalTangent, RemainingDelta) < 0.f) { @@ -323,8 +320,7 @@ FTengriSweepResult UTengriCollisionResolver::ResolveMovement( const UCapsuleComponent* Capsule, const FSurfaceThresholds& Thresholds, const float MaxStepHeight, - const int32 MaxIterations, - const bool bShowDebug) + const int32 MaxIterations) { FTengriSweepResult FinalResult; FinalResult.Location = StartLocation; @@ -336,7 +332,7 @@ FTengriSweepResult UTengriCollisionResolver::ResolveMovement( for (int32 Iteration = 0; Iteration < MaxIterations; ++Iteration) { const FVector Target = CurrentLocation + RemainingDelta; - const FTengriSweepResult Sweep = PerformSweep(WorldContext, CurrentLocation, Target, Capsule, bShowDebug); + const FTengriSweepResult Sweep = PerformSweep(WorldContext, CurrentLocation, Target, Capsule); FinalResult.CollisionCount++; diff --git a/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.h b/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.h index 17418bc..27757e3 100644 --- a/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.h +++ b/Source/TengriPlatformer/Movement/Collision/TengriCollisionResolver.h @@ -35,7 +35,6 @@ public: * @param Thresholds - Surface classification thresholds * @param MaxStepHeight - Maximum step-up height * @param MaxIterations - Maximum slide iterations - * @param bShowDebug - Enable debug visualization * @return Final position and collision info */ static FTengriSweepResult ResolveMovement( @@ -45,8 +44,7 @@ public: const UCapsuleComponent* Capsule, const FSurfaceThresholds& Thresholds, float MaxStepHeight, - int32 MaxIterations, - bool bShowDebug = false + int32 MaxIterations ); // ════════════════════════════════════════════════════════════════════ @@ -58,8 +56,7 @@ public: const UObject* WorldContext, const FVector& Start, const FVector& End, - const UCapsuleComponent* Capsule, - bool bShowDebug = false + const UCapsuleComponent* Capsule ); /** Attempt to step up over an obstacle */ diff --git a/Source/TengriPlatformer/Movement/TengriMovementComponent.cpp b/Source/TengriPlatformer/Movement/TengriMovementComponent.cpp index 45e5af4..ca70e52 100644 --- a/Source/TengriPlatformer/Movement/TengriMovementComponent.cpp +++ b/Source/TengriPlatformer/Movement/TengriMovementComponent.cpp @@ -128,8 +128,7 @@ void UTengriMovementComponent::SetJumpInput(const bool bPressed) JumpBufferTimer = MovementConfig->JumpBufferTime; } } - - // Обновляем состояние удержания + bIsJumpHeld = bPressed; } @@ -138,8 +137,8 @@ void UTengriMovementComponent::SetJumpInput(const bool bPressed) // ============================================================================ void UTengriMovementComponent::TickComponent( - float DeltaTime, - ELevelTick TickType, + const float DeltaTime, + const ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); @@ -346,27 +345,60 @@ void UTengriMovementComponent::TickPhysics( PhysicsVelocity = HorizontalVelocity; PhysicsVelocity.Z = CurrentZ; - // ════════════════════════════════════════════════════════════════════ - // Phase 4: Rotation - // ════════════════════════════════════════════════════════════════════ + // ════════════════════════════════════════════════════════════════════ + // Phase 4: Rotation + // ════════════════════════════════════════════════════════════════════ - if (const float MinSpeedSq = FMath::Square(MovementConfig->MinSpeedForRotation); - PhysicsVelocity.SizeSquared2D() > MinSpeedSq) - { - FRotator TargetRot = PhysicsVelocity.ToOrientationRotator(); - TargetRot.Pitch = 0.0f; - TargetRot.Roll = 0.0f; + FRotator TargetRot = PhysicsRotation; // Default: maintain current rotation + bool bShouldUpdateRotation = false; + float CurrentRotSpeed = MovementConfig->RotationSpeed; - PhysicsRotation = FMath::RInterpConstantTo( - PhysicsRotation, - TargetRot, - FixedDeltaTime, - MovementConfig->RotationSpeed - ); - } + // SCENARIO A: STRAFE MODE (Aiming) + // Character rotates to match camera even when stationary. + // Used for combat/precise aiming scenarios. + if (bStrafing) + { + if (const APawn* PawnOwner = Cast(GetOwner())) + { + if (const AController* C = PawnOwner->GetController()) + { + TargetRot = C->GetControlRotation(); + bShouldUpdateRotation = true; + + // Optional: Faster rotation in combat for responsive feel + CurrentRotSpeed *= 2.0f; + } + } + } + // SCENARIO B: STANDARD MOVEMENT (Classic platformer) + // Character rotates toward velocity direction only when moving. + // Maintains forward orientation based on movement. + else + { + if (const float MinSpeedSq = FMath::Square(MovementConfig->MinSpeedForRotation); PhysicsVelocity.SizeSquared2D() > MinSpeedSq) + { + TargetRot = PhysicsVelocity.ToOrientationRotator(); + bShouldUpdateRotation = true; + } + } + + // APPLY ROTATION + if (bShouldUpdateRotation) + { + // Always prevent pitch/roll to keep character upright + TargetRot.Pitch = 0.0f; + TargetRot.Roll = 0.0f; + + PhysicsRotation = FMath::RInterpConstantTo( + PhysicsRotation, + TargetRot, + FixedDeltaTime, + CurrentRotSpeed + ); + } // ════════════════════════════════════════════════════════════════════ - // Phase 5: Gravity (FIXED - Apply before collision) + // Phase 5: Gravity // ════════════════════════════════════════════════════════════════════ if (!bIsGrounded) @@ -401,8 +433,7 @@ void UTengriMovementComponent::TickPhysics( OwnerCapsule, CachedThresholds, MovementConfig->MaxStepHeight, - MovementConfig->MaxSlideIterations, - false + MovementConfig->MaxSlideIterations ); PhysicsLocation = MoveResult.Location; @@ -430,7 +461,7 @@ void UTengriMovementComponent::TickPhysics( } // ════════════════════════════════════════════════════════════════════ - // Phase 8: State Update (IMPROVED) + // Phase 8: State Update // ════════════════════════════════════════════════════════════════════ const bool bWasGrounded = bIsGrounded; @@ -444,7 +475,7 @@ void UTengriMovementComponent::TickPhysics( const bool bNowGrounded = bJustSnapped || bHitWalkable; // ════════════════════════════════════════════════════════════════════ - // Phase 9: Landing Detection (ACCUMULATED) + // Phase 9: Landing Detection // ════════════════════════════════════════════════════════════════════ if (!bWasGrounded && bNowGrounded) @@ -524,8 +555,7 @@ bool UTengriMovementComponent::PerformGroundSnapping( this, Start, End, - OwnerCapsule, - false + OwnerCapsule ); if (Sweep.bBlocked && CachedThresholds.IsWalkable(Sweep.Hit.ImpactNormal.Z)) @@ -537,4 +567,12 @@ bool UTengriMovementComponent::PerformGroundSnapping( } return false; +} + +void UTengriMovementComponent::ForceRotation(const FRotator& NewRotation) +{ + // Обновляем все внутренние состояния, чтобы физика "подхватила" новый поворот + PhysicsRotation = NewRotation; + RenderRotation = NewRotation; + PreviousPhysicsRotation = NewRotation; // Сбрасываем интерполяцию } \ No newline at end of file diff --git a/Source/TengriPlatformer/Movement/TengriMovementComponent.h b/Source/TengriPlatformer/Movement/TengriMovementComponent.h index 0ae272a..0730958 100644 --- a/Source/TengriPlatformer/Movement/TengriMovementComponent.h +++ b/Source/TengriPlatformer/Movement/TengriMovementComponent.h @@ -36,9 +36,18 @@ public: UPROPERTY(BlueprintAssignable, Category = "Tengri Movement|Events") FOnTengriLandingSignature OnLanded; + /** Instantly snaps character rotation to a new value (bypassing interpolation) */ + void ForceRotation(const FRotator& NewRotation); + + /** Enable strafe mode (character rotates toward camera instead of movement direction) */ + void SetStrafing(const bool bEnabled) { bStrafing = bEnabled; } + protected: virtual void BeginPlay() override; + /** Strafe mode flag */ + bool bStrafing = false; + public: virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; diff --git a/Source/TengriPlatformer/Movement/TengriMovementComponent.ts b/Source/TengriPlatformer/Movement/TengriMovementComponent.ts index d616cc8..0cc3e7a 100644 --- a/Source/TengriPlatformer/Movement/TengriMovementComponent.ts +++ b/Source/TengriPlatformer/Movement/TengriMovementComponent.ts @@ -1,17 +1,25 @@ // Source/TengriPlatformer/Movement/TengriMovementComponent.ts import { ActorComponent } from '/Content/UE/ActorComponent.ts'; -import type { Vector } from '/Content/UE/Vector.ts'; +import { Vector } from '/Content/UE/Vector.ts'; import type { DA_TengriMovementConfig } from '/Content/Movement/DA_TengriMovementConfig.ts'; +type ConstructorParams = { + MovementConfig: typeof DA_TengriMovementConfig; +}; + export class TengriMovementComponent extends ActorComponent { - constructor(MovementConfig: typeof DA_TengriMovementConfig) { + constructor(data?: ConstructorParams) { super(); - console.log(MovementConfig); + console.log(data); } - public SetInputVector(NewInput: Vector): void { + public SetInputVector(NewInput: Vector = new Vector()): void { console.log(NewInput); } + + public SetJumpInput(bPressed: boolean = false): void { + console.log(bPressed); + } } diff --git a/Source/TengriPlatformer/TengriPlatformer.Build.cs b/Source/TengriPlatformer/TengriPlatformer.Build.cs index 4b21d42..90a6442 100644 --- a/Source/TengriPlatformer/TengriPlatformer.Build.cs +++ b/Source/TengriPlatformer/TengriPlatformer.Build.cs @@ -4,13 +4,13 @@ using UnrealBuildTool; public class TengriPlatformer : ModuleRules { - public TengriPlatformer(ReadOnlyTargetRules Target) : base(Target) + public TengriPlatformer(ReadOnlyTargetRules target) : base(target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" }); + PublicDependencyModuleNames.AddRange(["Core", "CoreUObject", "Engine", "InputCore"]); - PrivateDependencyModuleNames.AddRange(new string[] { }); + PrivateDependencyModuleNames.AddRange(["EnhancedInput"]); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); diff --git a/Source/TengriPlatformer/World/TengriPickupActor.cpp b/Source/TengriPlatformer/World/TengriPickupActor.cpp new file mode 100644 index 0000000..ab4ef2c --- /dev/null +++ b/Source/TengriPlatformer/World/TengriPickupActor.cpp @@ -0,0 +1,82 @@ +// Request Games © All rights reserved + +// Source/TengriPlatformer/World/TengriPickupActor.cpp + +#include "TengriPickupActor.h" +#include "Components/StaticMeshComponent.h" +#include "Components/SphereComponent.h" + +ATengriPickupActor::ATengriPickupActor() +{ + PrimaryActorTick.bCanEverTick = false; // Физике тик не нужен + + // 1. Mesh Setup + MeshComp = CreateDefaultSubobject(TEXT("MeshComp")); + RootComponent = MeshComp; + + // Enable physics by default + MeshComp->SetSimulatePhysics(true); + MeshComp->SetCollisionProfileName(TEXT("PhysicsActor")); // Стандартный профиль UE для предметов + MeshComp->SetMassOverrideInKg(NAME_None, 10.0f); // Вес по дефолту + + // 2. Trigger Setup + TriggerComp = CreateDefaultSubobject(TEXT("TriggerComp")); + TriggerComp->SetupAttachment(MeshComp); + TriggerComp->SetSphereRadius(80.0f); + + // Trigger collision settings + TriggerComp->SetCollisionProfileName(TEXT("Trigger")); +} + +void ATengriPickupActor::BeginPlay() +{ + Super::BeginPlay(); +} + +void ATengriPickupActor::OnPickedUp(USceneComponent* AttachTo, const FName SocketName) +{ + if (!MeshComp || !AttachTo) return; + + bIsHeld = true; + + // 1. Disable Physics + MeshComp->SetSimulatePhysics(false); + + // 2. Disable Collision + MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); + + // 3. Attach to Hand + FAttachmentTransformRules AttachmentRules( + EAttachmentRule::SnapToTarget, // Location Rule + EAttachmentRule::SnapToTarget, // Rotation Rule + EAttachmentRule::KeepWorld, // Scale Rule + false // bWeldSimulatedBodies (добавили этот аргумент) + ); + + AttachToComponent(AttachTo, AttachmentRules, SocketName); +} + +void ATengriPickupActor::OnDropped(const FVector Impulse, const bool bVelChange) +{ + if (!MeshComp) return; + + bIsHeld = false; + + // 1. Detach + DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); + + // 2. Re-enable Collision + MeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + + if (!MeshComp) return; + + // 3. Re-enable Physics + MeshComp->SetSimulatePhysics(true); + + // 4. Apply Throw Force + if (!Impulse.IsNearlyZero()) + { + MeshComp->AddImpulse(Impulse, NAME_None, bVelChange); + } +} + diff --git a/Source/TengriPlatformer/World/TengriPickupActor.h b/Source/TengriPlatformer/World/TengriPickupActor.h new file mode 100644 index 0000000..a13ea85 --- /dev/null +++ b/Source/TengriPlatformer/World/TengriPickupActor.h @@ -0,0 +1,66 @@ +// Request Games © All rights reserved + +// Source/TengriPlatformer/World/TengriPickupActor.cpp + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "TengriPickupActor.generated.h" + +class USphereComponent; +class UStaticMeshComponent; + +/** + * Base class for all physics pickup items (rocks, bones, keys). + * Handles physics states when held/dropped. + */ +UCLASS() +class TENGRIPLATFORMER_API ATengriPickupActor : public AActor +{ + GENERATED_BODY() + +public: + ATengriPickupActor(); + +protected: + virtual void BeginPlay() override; + +public: + // ======================================================================== + // COMPONENTS + // ======================================================================== + + /** Visual representation and physics body */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pickup") + TObjectPtr MeshComp; + + /** Trigger zone for detection */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pickup") + TObjectPtr TriggerComp; + + // ======================================================================== + // STATE API + // ======================================================================== + + /** * Called when character picks up this item. + * Disables physics and collision. + */ + UFUNCTION(BlueprintCallable, Category = "Pickup") + virtual void OnPickedUp(USceneComponent* AttachTo, FName SocketName); + + /** * Called when character drops/throws this item. + * Re-enables physics and detaches. + * @param Impulse - Optional force to apply (for throwing) + * @param bVelChange + */ + UFUNCTION(BlueprintCallable, Category = "Pickup") + virtual void OnDropped(FVector Impulse = FVector::ZeroVector, bool bVelChange = false); + + /** Check if currently held by someone */ + UFUNCTION(BlueprintPure, Category = "Pickup") + bool IsHeld() const { return bIsHeld; } + +protected: + bool bIsHeld = false; +}; diff --git a/Source/TengriPlatformer/World/TengriPickupActor.ts b/Source/TengriPlatformer/World/TengriPickupActor.ts new file mode 100644 index 0000000..c273136 --- /dev/null +++ b/Source/TengriPlatformer/World/TengriPickupActor.ts @@ -0,0 +1,5 @@ +// Source/TengriPlatformer/World/TengriPickupActor.ts + +import { Actor } from '/Content/UE/Actor'; + +export class TengriPickupActor extends Actor {}