diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp index 9a2482c..9809d00 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp @@ -10,6 +10,10 @@ #include "InputMappingContext.h" #include "Engine/World.h" #include "TimerManager.h" +#include "Containers/Ticker.h" +#include "Kismet/GameplayStatics.h" +#include "Misc/CommandLine.h" +#include "Misc/Parse.h" AGigLineCharacter::AGigLineCharacter() { @@ -120,6 +124,31 @@ void AGigLineCharacter::BeginPlay() StartLocation = GetActorLocation(); StartRotation = GetActorRotation(); + // The PlayerStart sits above grade so the capsule cannot spawn embedded, so + // the BeginPlay position is mid-air. Re-capture once the pawn has landed, + // otherwise Reset drops the player 1.4 m back into the air every press. + // Repeat until the first grounded frame. A single late timer is wrong: it can + // fire after the player (or the QA self-test) has already started walking, + // which recorded a mid-stride position as "start". + GetWorldTimerManager().SetTimer(SettleHandle, [this]() + { + if (bQATestWalking) + { + return; // keep waiting; the test walks almost immediately + } + if (const UCharacterMovementComponent* Mov = GetCharacterMovement()) + { + if (Mov->IsMovingOnGround()) + { + StartLocation = GetActorLocation(); + StartRotation = GetActorRotation(); + GetWorldTimerManager().ClearTimer(this->SettleHandle); + UE_LOG(LogTemp, Log, TEXT("GigLine: start captured on ground at Z %.0f"), + StartLocation.Z); + } + } + }, 0.25f, true); + // Force game input mode + focus + mouse capture so keyboard/mouse reach the // pawn regardless of window focus quirks. if (APlayerController* PC = Cast(GetController())) @@ -167,6 +196,120 @@ void AGigLineCharacter::BeginPlay() bHit && Hit.GetActor() ? *Hit.GetActor()->GetActorLabel() : TEXT("none")); } }, 1.0f, true); + + if (FParse::Param(FCommandLine::Get(), TEXT("qaselftest"))) + { + StartQASelfTest(); + } +} + +void AGigLineCharacter::StartQASelfTest() +{ + // Driven by a core ticker rather than a world timer on purpose: pausing the + // game stops world timers, so a timer-based test would freeze the moment it + // proved pause works. + struct FQAState + { + float Elapsed = 0.0f; + FVector Start = FVector::ZeroVector; + FVector WalkEnd = FVector::ZeroVector; + float Step = 0.0f; + bool bWalkMoved = false; + bool bResetReturned = false; + bool bPausedOn = false; + bool bUnpausedBack = false; + }; + + TSharedPtr State = MakeShared(); + State->Start = GetActorLocation(); + UE_LOG(LogTemp, Log, TEXT("GigLine QA: start XYZ=(%.0f,%.0f,%.0f)"), + State->Start.X, State->Start.Y, State->Start.Z); + + FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateWeakLambda(this, + [this, State](float DeltaTime) + { + State->Elapsed += DeltaTime; + const float E = State->Elapsed; + + // 1.0 - 3.0 s: hold forward. Reset sets the Step marker so each phase + // runs exactly once without an ever-growing if-chain. + if (E >= 1.0f && State->Step < 1.0f) + { + State->Step = 1.0f; + // Measure from the settled position, not the mid-air spawn point. + State->Start = GetActorLocation(); + UE_LOG(LogTemp, Log, TEXT("GigLine QA: settled XYZ=(%.0f,%.0f,%.0f)"), + State->Start.X, State->Start.Y, State->Start.Z); + bQATestWalking = true; + } + if (bQATestWalking) + { + AddMovementInput(GetActorForwardVector(), 1.0f); + } + + // 3.0: stop and measure. + if (E >= 3.0f && State->Step < 2.0f) + { + State->Step = 2.0f; + bQATestWalking = false; + State->WalkEnd = GetActorLocation(); + const float Walked = FVector::Dist2D(State->Start, State->WalkEnd); + State->bWalkMoved = Walked > 200.0f; // >2 m in 2 s at 4 m/s + UE_LOG(LogTemp, Log, + TEXT("GigLine QA: walk moved %.0f cm in 2 s -> %s (Z %.0f -> %.0f)"), + Walked, State->bWalkMoved ? TEXT("PASS") : TEXT("FAIL"), + State->Start.Z, State->WalkEnd.Z); + } + + // 3.5: reset should return to the spawn point. + if (E >= 3.5f && State->Step < 3.0f) + { + State->Step = 3.0f; + ResetToStart(); + } + if (E >= 4.0f && State->Step < 4.0f) + { + State->Step = 4.0f; + const float Back = FVector::Dist(State->Start, GetActorLocation()); + State->bResetReturned = Back < 100.0f; + UE_LOG(LogTemp, Log, TEXT("GigLine QA: reset returned %.0f cm from spawn -> %s"), + Back, State->bResetReturned ? TEXT("PASS") : TEXT("FAIL")); + } + + // 4.5: pause, then confirm the world actually stopped. + if (E >= 4.5f && State->Step < 5.0f) + { + State->Step = 5.0f; + TogglePause(); + State->bPausedOn = UGameplayStatics::IsGamePaused(this); + UE_LOG(LogTemp, Log, TEXT("GigLine QA: pause -> paused=%d %s"), + State->bPausedOn ? 1 : 0, State->bPausedOn ? TEXT("PASS") : TEXT("FAIL")); + } + + // 5.5: unpause and confirm it comes back. + if (E >= 5.5f && State->Step < 6.0f) + { + State->Step = 6.0f; + TogglePause(); + State->bUnpausedBack = !UGameplayStatics::IsGamePaused(this); + UE_LOG(LogTemp, Log, TEXT("GigLine QA: unpause -> paused=%d %s"), + UGameplayStatics::IsGamePaused(this) ? 1 : 0, + State->bUnpausedBack ? TEXT("PASS") : TEXT("FAIL")); + } + + // 6.0: verdict, then stop ticking. + if (E >= 6.0f) + { + const bool bAllPass = State->bWalkMoved && State->bResetReturned + && State->bPausedOn && State->bUnpausedBack; + UE_LOG(LogTemp, Log, TEXT("GigLine QA: RESULT walk=%d reset=%d pause=%d unpause=%d -> %s"), + State->bWalkMoved ? 1 : 0, State->bResetReturned ? 1 : 0, + State->bPausedOn ? 1 : 0, State->bUnpausedBack ? 1 : 0, + bAllPass ? TEXT("PASS") : TEXT("FAIL")); + return false; + } + return true; + })); } void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.h b/Game/GigLine/Source/GigLine/GigLineCharacter.h index a0313a6..08c5e66 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.h +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.h @@ -38,6 +38,22 @@ protected: void ResetToStart(); void TogglePause(); + /** + * TEMP QA: scripted walk / reset / pause verification. + * + * Enabled only by the `-qaselftest` command-line switch, so it never runs in + * normal play. It exists because those three behaviours were listed as + * "needs a human at the keyboard" and a repeatable headless check beats + * waiting for one. + */ + void StartQASelfTest(); + + /** Set while the self-test is driving forward input. */ + bool bQATestWalking = false; + + /** Repeating timer that captures StartLocation on first ground contact. */ + FTimerHandle SettleHandle; + /** Where the player started, so reset returns there rather than the origin. */ UPROPERTY() FVector StartLocation = FVector::ZeroVector;