From ad3e9ad19d1b48a9bc1d00ea3805434d9d73d01a Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 29 Apr 2026 04:09:36 +0200 Subject: [PATCH] Retain drill recovery history on dashboard --- .../HyperTwistCoachDashboardWidget.cpp | 275 ++++++++++++++++++ .../HyperTwistCoachDashboardWidget.h | 37 +++ 2 files changed, 312 insertions(+) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 99c2d95..3cd0cd7 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -760,6 +760,21 @@ namespace HyperTwistCoachDashboardWidgetInternal + Bias.MethodDrillFollowUpRecommendationCount * 2 + Bias.MethodDrillRecommendedRecommendationCount; } + + void CollectMethodDrillRecoveryOutcomeIndices( + const TArray& Outcomes, + TArray& OutIndices) + { + OutIndices.Reset(); + for (int32 Index = 0; Index < Outcomes.Num(); ++Index) + { + const FHyperTwistCoachDashboardQueueRecoveryOutcome& Outcome = Outcomes[Index]; + if (Outcome.IsStructurallyValid() && Outcome.bUsedMethodDrillHandoffSignal) + { + OutIndices.Add(Index); + } + } + } } void UHyperTwistCoachDashboardWidget::NativeConstruct() @@ -811,6 +826,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() SyncSelectedQueueSuppressionHistoryEntry(); SyncSelectedClosureRecoveryHistoryEntry(); SyncSelectedMethodDrillFollowUpHistoryEntry(); + SyncSelectedMethodDrillRecoveryOutcomeEntry(); SyncPreferredGuidancePreviewLane(false); UpdateDashboardPresentation(); } @@ -1876,6 +1892,75 @@ bool UHyperTwistCoachDashboardWidget::SelectNextMethodDrillFollowUpHistoryEntry( return true; } +bool UHyperTwistCoachDashboardWidget::SelectPreviousMethodDrillRecoveryOutcomeEntry() +{ + TArray MethodDrillRecoveryIndices; + HyperTwistCoachDashboardWidgetInternal::CollectMethodDrillRecoveryOutcomeIndices( + QueueRecoveryOutcomeHistory, + MethodDrillRecoveryIndices + ); + if (MethodDrillRecoveryIndices.Num() <= 0) + { + SelectedMethodDrillRecoveryOutcomeIndex = INDEX_NONE; + return false; + } + + if (SelectedMethodDrillRecoveryOutcomeIndex == INDEX_NONE) + { + SyncSelectedMethodDrillRecoveryOutcomeEntry(); + return SelectedMethodDrillRecoveryOutcomeIndex != INDEX_NONE; + } + + const int32 CurrentFilteredIndex = + MethodDrillRecoveryIndices.IndexOfByKey(SelectedMethodDrillRecoveryOutcomeIndex); + if (CurrentFilteredIndex == INDEX_NONE) + { + SyncSelectedMethodDrillRecoveryOutcomeEntry(); + return SelectedMethodDrillRecoveryOutcomeIndex != INDEX_NONE; + } + + SelectedMethodDrillRecoveryOutcomeIndex = + MethodDrillRecoveryIndices[ + (CurrentFilteredIndex - 1 + MethodDrillRecoveryIndices.Num()) + % MethodDrillRecoveryIndices.Num()]; + UpdateDashboardPresentation(); + return true; +} + +bool UHyperTwistCoachDashboardWidget::SelectNextMethodDrillRecoveryOutcomeEntry() +{ + TArray MethodDrillRecoveryIndices; + HyperTwistCoachDashboardWidgetInternal::CollectMethodDrillRecoveryOutcomeIndices( + QueueRecoveryOutcomeHistory, + MethodDrillRecoveryIndices + ); + if (MethodDrillRecoveryIndices.Num() <= 0) + { + SelectedMethodDrillRecoveryOutcomeIndex = INDEX_NONE; + return false; + } + + if (SelectedMethodDrillRecoveryOutcomeIndex == INDEX_NONE) + { + SyncSelectedMethodDrillRecoveryOutcomeEntry(); + return SelectedMethodDrillRecoveryOutcomeIndex != INDEX_NONE; + } + + const int32 CurrentFilteredIndex = + MethodDrillRecoveryIndices.IndexOfByKey(SelectedMethodDrillRecoveryOutcomeIndex); + if (CurrentFilteredIndex == INDEX_NONE) + { + SyncSelectedMethodDrillRecoveryOutcomeEntry(); + return SelectedMethodDrillRecoveryOutcomeIndex != INDEX_NONE; + } + + SelectedMethodDrillRecoveryOutcomeIndex = + MethodDrillRecoveryIndices[ + (CurrentFilteredIndex + 1) % MethodDrillRecoveryIndices.Num()]; + UpdateDashboardPresentation(); + return true; +} + bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer() { if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()) @@ -2222,6 +2307,16 @@ void UHyperTwistCoachDashboardWidget::HandleNextMethodDrillFollowUpHistoryClicke SelectNextMethodDrillFollowUpHistoryEntry(); } +void UHyperTwistCoachDashboardWidget::HandlePreviousMethodDrillRecoveryHistoryClicked() +{ + SelectPreviousMethodDrillRecoveryOutcomeEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleNextMethodDrillRecoveryHistoryClicked() +{ + SelectNextMethodDrillRecoveryOutcomeEntry(); +} + void UHyperTwistCoachDashboardWidget::HandleClearRunClicked() { ClearActiveTrainingRun(); @@ -3146,6 +3241,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachQueueRecoveryOutcomeDetail"), 8 ); + MethodDrillRecoveryHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillRecoveryHistoryHeader"), + 2 + ); + MethodDrillRecoveryHistoryStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillRecoveryHistoryStatus"), + 2 + ); + MethodDrillRecoveryHistoryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillRecoveryHistoryDetail"), + 8 + ); QueueSuppressionHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -3318,6 +3431,41 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() NextClosureRecoveryButtonLabel = NextClosureRecoveryLabelRaw; } + UHorizontalBox* MethodDrillRecoveryNavRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardMethodDrillRecoveryNavRow") + ); + if (MethodDrillRecoveryNavRow != nullptr) + { + if (UVerticalBoxSlot* MethodDrillRecoveryNavRowSlot = + RootLayout->AddChildToVerticalBox(MethodDrillRecoveryNavRow)) + { + MethodDrillRecoveryNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PreviousMethodDrillRecoveryLabelRaw = nullptr; + PreviousMethodDrillRecoveryHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + MethodDrillRecoveryNavRow, + TEXT("PreviousMethodDrillRecoveryHistoryButton"), + TEXT("PreviousMethodDrillRecoveryHistoryButtonLabel"), + TEXT("Prev Drill Recovery"), + PreviousMethodDrillRecoveryLabelRaw + ); + PreviousMethodDrillRecoveryHistoryButtonLabel = PreviousMethodDrillRecoveryLabelRaw; + + UTextBlock* NextMethodDrillRecoveryLabelRaw = nullptr; + NextMethodDrillRecoveryHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + MethodDrillRecoveryNavRow, + TEXT("NextMethodDrillRecoveryHistoryButton"), + TEXT("NextMethodDrillRecoveryHistoryButtonLabel"), + TEXT("Next Drill Recovery"), + NextMethodDrillRecoveryLabelRaw + ); + NextMethodDrillRecoveryHistoryButtonLabel = NextMethodDrillRecoveryLabelRaw; + } + UHorizontalBox* AttemptInputRow = WidgetTree->ConstructWidget( UHorizontalBox::StaticClass(), TEXT("CoachDashboardAttemptInputRow") @@ -3731,6 +3879,20 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() &UHyperTwistCoachDashboardWidget::HandleNextMethodDrillFollowUpHistoryClicked ); } + if (PreviousMethodDrillRecoveryHistoryButton != nullptr) + { + PreviousMethodDrillRecoveryHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandlePreviousMethodDrillRecoveryHistoryClicked + ); + } + if (NextMethodDrillRecoveryHistoryButton != nullptr) + { + NextMethodDrillRecoveryHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandleNextMethodDrillRecoveryHistoryClicked + ); + } if (CompleteRunButton != nullptr) { CompleteRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked); @@ -4012,6 +4174,25 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedMethodDrillFollowUpHistoryEntr } } +void UHyperTwistCoachDashboardWidget::SyncSelectedMethodDrillRecoveryOutcomeEntry() +{ + TArray MethodDrillRecoveryIndices; + HyperTwistCoachDashboardWidgetInternal::CollectMethodDrillRecoveryOutcomeIndices( + QueueRecoveryOutcomeHistory, + MethodDrillRecoveryIndices + ); + if (MethodDrillRecoveryIndices.Num() <= 0) + { + SelectedMethodDrillRecoveryOutcomeIndex = INDEX_NONE; + return; + } + + if (!MethodDrillRecoveryIndices.Contains(SelectedMethodDrillRecoveryOutcomeIndex)) + { + SelectedMethodDrillRecoveryOutcomeIndex = MethodDrillRecoveryIndices.Last(); + } +} + void UHyperTwistCoachDashboardWidget::SyncPreferredGuidancePreviewLane(const bool bForceAdoptPreferredLane) { const FHyperTwistCoachDashboardGuidanceLanePreference Preference = BuildGuidanceLanePreference(); @@ -5161,6 +5342,7 @@ void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( const int32 ExcessCount = QueueRecoveryOutcomeHistory.Num() - MaxRetainedQueueRecoveryOutcomes; QueueRecoveryOutcomeHistory.RemoveAt(0, ExcessCount, EAllowShrinking::No); } + SyncSelectedMethodDrillRecoveryOutcomeEntry(); } } @@ -6961,6 +7143,64 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() QueueRecoveryOutcome.bUsedDeferredFirstSuppressionSignal ? TEXT("yes") : TEXT("no"), QueueRecoveryOutcome.bUsedMethodDrillHandoffSignal ? TEXT("yes") : TEXT("no") ); + TArray MethodDrillRecoveryHistoryIndices; + HyperTwistCoachDashboardWidgetInternal::CollectMethodDrillRecoveryOutcomeIndices( + QueueRecoveryOutcomeHistory, + MethodDrillRecoveryHistoryIndices + ); + const int32 SelectedMethodDrillRecoveryHistoryPosition = + MethodDrillRecoveryHistoryIndices.IndexOfByKey(SelectedMethodDrillRecoveryOutcomeIndex); + const FHyperTwistCoachDashboardQueueRecoveryOutcome* SelectedMethodDrillRecoveryOutcome = + SelectedMethodDrillRecoveryOutcomeIndex >= 0 + && SelectedMethodDrillRecoveryOutcomeIndex < QueueRecoveryOutcomeHistory.Num() + ? &QueueRecoveryOutcomeHistory[SelectedMethodDrillRecoveryOutcomeIndex] + : nullptr; + const FString MethodDrillRecoveryHistoryVerdictLine = + SelectedMethodDrillRecoveryOutcome == nullptr || !SelectedMethodDrillRecoveryOutcome->IsStructurallyValid() + ? TEXT("n/a") + : (!SelectedMethodDrillRecoveryOutcome->bSucceeded + ? TEXT("failed") + : (SelectedMethodDrillRecoveryOutcome->bReducedFriction + ? TEXT("reduced friction") + : (SelectedMethodDrillRecoveryOutcome->bShiftedFriction + ? TEXT("shifted friction") + : TEXT("no immediate change")))); + const FString MethodDrillRecoveryHistorySourceLine = + SelectedMethodDrillRecoveryOutcome == nullptr + ? TEXT("n/a") + : HyperTwistCoachDashboardWidgetInternal::DescribeCoachMemorySourceLabel( + SelectedMethodDrillRecoveryOutcome->SourceLabel); + const FString MethodDrillRecoveryHistoryStatusLine = + SelectedMethodDrillRecoveryOutcome != nullptr + && SelectedMethodDrillRecoveryOutcome->IsStructurallyValid() + && SelectedMethodDrillRecoveryHistoryPosition != INDEX_NONE + ? FString::Printf( + TEXT("Drill recovery history: %d entries | selected %d/%d | %s | %s | Recorded: %s"), + MethodDrillRecoveryHistoryIndices.Num(), + SelectedMethodDrillRecoveryHistoryPosition + 1, + MethodDrillRecoveryHistoryIndices.Num(), + *SelectedMethodDrillRecoveryOutcome->AppliedActionLabel, + *MethodDrillRecoveryHistoryVerdictLine, + *SelectedMethodDrillRecoveryOutcome->RecordedAtUtc) + : TEXT("Drill recovery history: no retained drill-weighted queue-recovery outcomes recorded yet."); + const FString MethodDrillRecoveryHistoryDetailLine = + SelectedMethodDrillRecoveryOutcome != nullptr + && SelectedMethodDrillRecoveryOutcome->IsStructurallyValid() + ? FString::Printf( + TEXT("Source: %s | Pressure %.2f -> %.2f | Deferred %d -> %d | Blocked follow-up %s -> %s | Outstanding deferred %s -> %s | Completion gap %s -> %s | deferred-first weighted: %s"), + *MethodDrillRecoveryHistorySourceLine, + SelectedMethodDrillRecoveryOutcome->BeforePressureScore, + SelectedMethodDrillRecoveryOutcome->AfterPressureScore, + SelectedMethodDrillRecoveryOutcome->BeforeDeferredEventCount, + SelectedMethodDrillRecoveryOutcome->AfterDeferredEventCount, + SelectedMethodDrillRecoveryOutcome->bBeforeBlockedFollowUp ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillRecoveryOutcome->bAfterBlockedFollowUp ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillRecoveryOutcome->bBeforeOutstandingDeferred ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillRecoveryOutcome->bAfterOutstandingDeferred ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillRecoveryOutcome->bBeforeCompletionGap ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillRecoveryOutcome->bAfterCompletionGap ? TEXT("yes") : TEXT("no"), + SelectedMethodDrillRecoveryOutcome->bUsedDeferredFirstSuppressionSignal ? TEXT("yes") : TEXT("no")) + : TEXT("Drill recovery detail: when drill-weighted queue recovery actions are applied, their retained before/after friction outcomes will accumulate here for later comparison against generic recovery."); const FString PreferredDeferredRecoveryEntryId = FindFirstDeferredQueueEntryId(); const bool bCanApplyQueueRecovery = !bHasLiveAttemptTimer @@ -8356,6 +8596,41 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { QueueRecoveryOutcomeDetailTextBlock->SetText(FText::FromString(QueueRecoveryOutcomeDetailLine)); } + if (MethodDrillRecoveryHistoryHeaderTextBlock != nullptr) + { + MethodDrillRecoveryHistoryHeaderTextBlock->SetText(FText::FromString( + TEXT("[Drill Recovery History]"))); + } + if (MethodDrillRecoveryHistoryStatusTextBlock != nullptr) + { + MethodDrillRecoveryHistoryStatusTextBlock->SetText(FText::FromString( + MethodDrillRecoveryHistoryStatusLine)); + } + if (MethodDrillRecoveryHistoryDetailTextBlock != nullptr) + { + MethodDrillRecoveryHistoryDetailTextBlock->SetText(FText::FromString( + MethodDrillRecoveryHistoryDetailLine)); + } + if (PreviousMethodDrillRecoveryHistoryButtonLabel != nullptr) + { + PreviousMethodDrillRecoveryHistoryButtonLabel->SetText(FText::FromString( + TEXT("Prev Drill Recovery"))); + } + if (PreviousMethodDrillRecoveryHistoryButton != nullptr) + { + PreviousMethodDrillRecoveryHistoryButton->SetIsEnabled( + MethodDrillRecoveryHistoryIndices.Num() > 1); + } + if (NextMethodDrillRecoveryHistoryButtonLabel != nullptr) + { + NextMethodDrillRecoveryHistoryButtonLabel->SetText(FText::FromString( + TEXT("Next Drill Recovery"))); + } + if (NextMethodDrillRecoveryHistoryButton != nullptr) + { + NextMethodDrillRecoveryHistoryButton->SetIsEnabled( + MethodDrillRecoveryHistoryIndices.Num() > 1); + } if (QueueSuppressionHistoryHeaderTextBlock != nullptr) { QueueSuppressionHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Queue Suppression History]"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 548fb42..4939265 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -387,6 +387,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedMethodDrillFollowUpHistoryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") + int32 SelectedMethodDrillRecoveryOutcomeIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") bool bHasLiveAttemptTimer = false; @@ -498,6 +501,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectNextMethodDrillFollowUpHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectPreviousMethodDrillRecoveryOutcomeEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectNextMethodDrillRecoveryOutcomeEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool StartLiveAttemptTimer(); @@ -879,6 +888,27 @@ protected: UPROPERTY(Transient) TObjectPtr QueueRecoveryOutcomeDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr MethodDrillRecoveryHistoryHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr MethodDrillRecoveryHistoryStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr MethodDrillRecoveryHistoryDetailTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousMethodDrillRecoveryHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousMethodDrillRecoveryHistoryButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextMethodDrillRecoveryHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextMethodDrillRecoveryHistoryButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr QueueSuppressionHistoryHeaderTextBlock = nullptr; @@ -1152,6 +1182,12 @@ protected: UFUNCTION() void HandleNextMethodDrillFollowUpHistoryClicked(); + UFUNCTION() + void HandlePreviousMethodDrillRecoveryHistoryClicked(); + + UFUNCTION() + void HandleNextMethodDrillRecoveryHistoryClicked(); + UFUNCTION() void HandleClearRunClicked(); @@ -1211,6 +1247,7 @@ private: void SyncSelectedQueueSuppressionHistoryEntry(); void SyncSelectedClosureRecoveryHistoryEntry(); void SyncSelectedMethodDrillFollowUpHistoryEntry(); + void SyncSelectedMethodDrillRecoveryOutcomeEntry(); void SyncPreferredGuidancePreviewLane(bool bForceAdoptPreferredLane); void UpdateDashboardPresentation(); void RefreshLiveAttemptClock();