From cc61288e8281813c9510872bdcbab2905fc2915a Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 05:26:52 +0200 Subject: [PATCH] Add playable coach dashboard run controls --- .../HyperTwistCoachDashboardWidget.cpp | 116 ++++++++++++++++++ .../HyperTwistCoachDashboardWidget.h | 33 +++++ 2 files changed, 149 insertions(+) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 8ab3bad..3566f83 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -203,6 +203,35 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartSelectedQueueE return FHyperTwistTrainingRunState(); } +FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitSyntheticCurrentAttempt( + const EHyperTwistTrainingAttemptResult Result, + const int32 TimeMs +) +{ + if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()) + { + return FHyperTwistTrainingRunStepResult(); + } + + FHyperTwistTrainingAttempt Attempt; + Attempt.AttemptId = FString::Printf( + TEXT("attempt_%s_%s"), + *CachedRunState.Session.TrainingSessionId, + *FGuid::NewGuid().ToString(EGuidFormats::Digits) + ); + Attempt.TrainingSessionId = CachedRunState.Session.TrainingSessionId; + Attempt.CaseId = CachedRunState.CurrentSelection.TrainingCase.CaseId; + Attempt.Result = Result; + Attempt.TotalTimeMs = FMath::Max(TimeMs, 0); + Attempt.ExecutionTimeMs = FMath::Max(TimeMs, 0); + Attempt.TimingBreakdown.RawSolveTimeMs = FMath::Max(TimeMs, 0); + Attempt.TimingBreakdown.FinalTimeMs = FMath::Max(TimeMs, 0); + Attempt.ReplayId = CachedRunState.ReplayPacket.ReplayId; + Attempt.CompletedAtUtc = FDateTime::UtcNow().ToIso8601(); + + return SubmitTrainingAttempt(Attempt, TimeMs); +} + void UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked() { ExecutePrimaryCoachAction(); @@ -252,6 +281,20 @@ void UHyperTwistCoachDashboardWidget::HandleClearRunClicked() UpdateDashboardPresentation(); } +void UHyperTwistCoachDashboardWidget::HandleRecordSuccessClicked() +{ + SubmitSyntheticCurrentAttempt(EHyperTwistTrainingAttemptResult::Success, DefaultSuccessAttemptTimeMs); + SyncSelectedQueueEntry(); + UpdateDashboardPresentation(); +} + +void UHyperTwistCoachDashboardWidget::HandleRecordFailureClicked() +{ + SubmitSyntheticCurrentAttempt(EHyperTwistTrainingAttemptResult::Failure, DefaultFailureAttemptTimeMs); + SyncSelectedQueueEntry(); + UpdateDashboardPresentation(); +} + void UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked() { SelectPreviousQueueEntry(); @@ -316,6 +359,12 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() WidgetTree, RootLayout, TEXT("CoachActiveRunSummary"), + 2 + ); + LastAttemptTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachLastAttempt"), 8 ); FollowUpTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( @@ -479,6 +528,28 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() ClearRunLabelRaw ); ClearRunButtonLabel = ClearRunLabelRaw; + + UTextBlock* RecordSuccessLabelRaw = nullptr; + RecordSuccessButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + RunControlRow, + TEXT("RecordSuccessButton"), + TEXT("RecordSuccessButtonLabel"), + TEXT("Record Success"), + RecordSuccessLabelRaw + ); + RecordSuccessButtonLabel = RecordSuccessLabelRaw; + + UTextBlock* RecordFailureLabelRaw = nullptr; + RecordFailureButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + RunControlRow, + TEXT("RecordFailureButton"), + TEXT("RecordFailureButtonLabel"), + TEXT("Record Failure"), + RecordFailureLabelRaw + ); + RecordFailureButtonLabel = RecordFailureLabelRaw; } UHorizontalBox* QueueNavRow = WidgetTree->ConstructWidget( @@ -550,6 +621,14 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() { ClearRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleClearRunClicked); } + if (RecordSuccessButton != nullptr) + { + RecordSuccessButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleRecordSuccessClicked); + } + if (RecordFailureButton != nullptr) + { + RecordFailureButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleRecordFailureClicked); + } if (PreviousQueueButton != nullptr) { PreviousQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked); @@ -628,6 +707,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() const FString ActiveDeckLabel = CachedRunState.ActiveDeck.Title.IsEmpty() ? (CachedRunState.ActiveDeck.DeckId.IsEmpty() ? TEXT("n/a") : CachedRunState.ActiveDeck.DeckId) : CachedRunState.ActiveDeck.Title; + const bool bHasRunnableCurrentCase = bHasActiveRun && CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid(); const FString VerificationNotePreview = CachedRepositoryRoundTripVerification.VerificationNotes.Num() > 0 ? CachedRepositoryRoundTripVerification.VerificationNotes[0] : TEXT("none"); @@ -683,6 +763,26 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() CachedRunSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *CachedRunSummary.LastCaseId ))); } + if (LastAttemptTextBlock != nullptr) + { + if (!LastStepResult.SessionSummary.TrainingSessionId.IsEmpty()) + { + LastAttemptTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Last recorded attempt: case %s | completed at %s | session completed attempts %d"), + LastStepResult.SessionSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *LastStepResult.SessionSummary.LastCaseId, + LastStepResult.SessionSummary.LastCompletedAtUtc.IsEmpty() + ? TEXT("n/a") + : *LastStepResult.SessionSummary.LastCompletedAtUtc, + LastStepResult.SessionSummary.CompletedAttemptCount + ))); + } + else + { + LastAttemptTextBlock->SetText(FText::FromString( + TEXT("Last recorded attempt: none in this dashboard session yet.") + )); + } + } if (FollowUpTextBlock != nullptr) { const FString FollowUpLine = @@ -912,6 +1012,22 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { ClearRunButton->SetIsEnabled(bHasActiveRun); } + if (RecordSuccessButtonLabel != nullptr) + { + RecordSuccessButtonLabel->SetText(FText::FromString(TEXT("Record Success"))); + } + if (RecordSuccessButton != nullptr) + { + RecordSuccessButton->SetIsEnabled(bHasRunnableCurrentCase); + } + if (RecordFailureButtonLabel != nullptr) + { + RecordFailureButtonLabel->SetText(FText::FromString(TEXT("Record Failure"))); + } + if (RecordFailureButton != nullptr) + { + RecordFailureButton->SetIsEnabled(bHasRunnableCurrentCase); + } if (PreviousQueueButtonLabel != nullptr) { PreviousQueueButtonLabel->SetText(FText::FromString(TEXT("Prev Queue"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 45b12cc..06d04da 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -19,6 +19,12 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard") int32 DefaultDeferredHours = 24; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard") + int32 DefaultSuccessAttemptTimeMs = 15000; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard") + int32 DefaultFailureAttemptTimeMs = 30000; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard") bool bPreferActiveQueueSelectionOnRefresh = true; @@ -46,6 +52,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") FHyperTwistTrainingRunState StartSelectedQueueEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingRunStepResult SubmitSyntheticCurrentAttempt( + EHyperTwistTrainingAttemptResult Result, + int32 TimeMs + ); + protected: UPROPERTY(Transient) TObjectPtr RootLayout = nullptr; @@ -68,6 +80,9 @@ protected: UPROPERTY(Transient) TObjectPtr ActiveRunSummaryTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr LastAttemptTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr QueueStatusTextBlock = nullptr; @@ -140,6 +155,18 @@ protected: UPROPERTY(Transient) TObjectPtr ClearRunButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr RecordSuccessButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr RecordSuccessButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr RecordFailureButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr RecordFailureButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr PreviousQueueButton = nullptr; @@ -176,6 +203,12 @@ protected: UFUNCTION() void HandleClearRunClicked(); + UFUNCTION() + void HandleRecordSuccessClicked(); + + UFUNCTION() + void HandleRecordFailureClicked(); + UFUNCTION() void HandlePreviousQueueClicked();