From 1b556633316b96719eab1220beb0203f7ee03dfd Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 29 Apr 2026 04:36:57 +0200 Subject: [PATCH] Retain drill recovery memory history on dashboard --- .../HyperTwistCoachDashboardWidget.cpp | 358 ++++++++++++++++++ .../HyperTwistCoachDashboardWidget.h | 72 ++++ 2 files changed, 430 insertions(+) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index c3b4586..741e5c8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -835,6 +835,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() SyncSelectedClosureRecoveryHistoryEntry(); SyncSelectedMethodDrillFollowUpHistoryEntry(); SyncSelectedMethodDrillRecoveryOutcomeEntry(); + SyncSelectedMethodDrillRecoveryMemoryHistoryEntry(); SyncPreferredGuidancePreviewLane(false); UpdateDashboardPresentation(); } @@ -1969,6 +1970,49 @@ bool UHyperTwistCoachDashboardWidget::SelectNextMethodDrillRecoveryOutcomeEntry( return true; } +bool UHyperTwistCoachDashboardWidget::SelectPreviousMethodDrillRecoveryMemoryHistoryEntry() +{ + if (MethodDrillRecoveryMemoryHistoryEntries.Num() <= 0) + { + SelectedMethodDrillRecoveryMemoryHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedMethodDrillRecoveryMemoryHistoryIndex == INDEX_NONE) + { + SyncSelectedMethodDrillRecoveryMemoryHistoryEntry(); + return SelectedMethodDrillRecoveryMemoryHistoryIndex != INDEX_NONE; + } + + SelectedMethodDrillRecoveryMemoryHistoryIndex = + (SelectedMethodDrillRecoveryMemoryHistoryIndex - 1 + + MethodDrillRecoveryMemoryHistoryEntries.Num()) + % MethodDrillRecoveryMemoryHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + +bool UHyperTwistCoachDashboardWidget::SelectNextMethodDrillRecoveryMemoryHistoryEntry() +{ + if (MethodDrillRecoveryMemoryHistoryEntries.Num() <= 0) + { + SelectedMethodDrillRecoveryMemoryHistoryIndex = INDEX_NONE; + return false; + } + + if (SelectedMethodDrillRecoveryMemoryHistoryIndex == INDEX_NONE) + { + SyncSelectedMethodDrillRecoveryMemoryHistoryEntry(); + return SelectedMethodDrillRecoveryMemoryHistoryIndex != INDEX_NONE; + } + + SelectedMethodDrillRecoveryMemoryHistoryIndex = + (SelectedMethodDrillRecoveryMemoryHistoryIndex + 1) + % MethodDrillRecoveryMemoryHistoryEntries.Num(); + UpdateDashboardPresentation(); + return true; +} + bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer() { if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()) @@ -2325,6 +2369,16 @@ void UHyperTwistCoachDashboardWidget::HandleNextMethodDrillRecoveryHistoryClicke SelectNextMethodDrillRecoveryOutcomeEntry(); } +void UHyperTwistCoachDashboardWidget::HandlePreviousMethodDrillRecoveryMemoryHistoryClicked() +{ + SelectPreviousMethodDrillRecoveryMemoryHistoryEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleNextMethodDrillRecoveryMemoryHistoryClicked() +{ + SelectNextMethodDrillRecoveryMemoryHistoryEntry(); +} + void UHyperTwistCoachDashboardWidget::HandleClearRunClicked() { ClearActiveTrainingRun(); @@ -3267,6 +3321,27 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachMethodDrillRecoveryHistoryDetail"), 8 ); + MethodDrillRecoveryMemoryHistoryHeaderTextBlock = + HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillRecoveryMemoryHistoryHeader"), + 2 + ); + MethodDrillRecoveryMemoryHistoryStatusTextBlock = + HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillRecoveryMemoryHistoryStatus"), + 2 + ); + MethodDrillRecoveryMemoryHistoryDetailTextBlock = + HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachMethodDrillRecoveryMemoryHistoryDetail"), + 8 + ); QueueSuppressionHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -3474,6 +3549,45 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() NextMethodDrillRecoveryHistoryButtonLabel = NextMethodDrillRecoveryLabelRaw; } + UHorizontalBox* MethodDrillRecoveryMemoryNavRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardMethodDrillRecoveryMemoryNavRow") + ); + if (MethodDrillRecoveryMemoryNavRow != nullptr) + { + if (UVerticalBoxSlot* MethodDrillRecoveryMemoryNavRowSlot = + RootLayout->AddChildToVerticalBox(MethodDrillRecoveryMemoryNavRow)) + { + MethodDrillRecoveryMemoryNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PreviousMethodDrillRecoveryMemoryLabelRaw = nullptr; + PreviousMethodDrillRecoveryMemoryHistoryButton = + HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + MethodDrillRecoveryMemoryNavRow, + TEXT("PreviousMethodDrillRecoveryMemoryHistoryButton"), + TEXT("PreviousMethodDrillRecoveryMemoryHistoryButtonLabel"), + TEXT("Prev Drill Memory"), + PreviousMethodDrillRecoveryMemoryLabelRaw + ); + PreviousMethodDrillRecoveryMemoryHistoryButtonLabel = + PreviousMethodDrillRecoveryMemoryLabelRaw; + + UTextBlock* NextMethodDrillRecoveryMemoryLabelRaw = nullptr; + NextMethodDrillRecoveryMemoryHistoryButton = + HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + MethodDrillRecoveryMemoryNavRow, + TEXT("NextMethodDrillRecoveryMemoryHistoryButton"), + TEXT("NextMethodDrillRecoveryMemoryHistoryButtonLabel"), + TEXT("Next Drill Memory"), + NextMethodDrillRecoveryMemoryLabelRaw + ); + NextMethodDrillRecoveryMemoryHistoryButtonLabel = + NextMethodDrillRecoveryMemoryLabelRaw; + } + UHorizontalBox* AttemptInputRow = WidgetTree->ConstructWidget( UHorizontalBox::StaticClass(), TEXT("CoachDashboardAttemptInputRow") @@ -3901,6 +4015,20 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() &UHyperTwistCoachDashboardWidget::HandleNextMethodDrillRecoveryHistoryClicked ); } + if (PreviousMethodDrillRecoveryMemoryHistoryButton != nullptr) + { + PreviousMethodDrillRecoveryMemoryHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandlePreviousMethodDrillRecoveryMemoryHistoryClicked + ); + } + if (NextMethodDrillRecoveryMemoryHistoryButton != nullptr) + { + NextMethodDrillRecoveryMemoryHistoryButton->OnClicked.AddDynamic( + this, + &UHyperTwistCoachDashboardWidget::HandleNextMethodDrillRecoveryMemoryHistoryClicked + ); + } if (CompleteRunButton != nullptr) { CompleteRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked); @@ -4201,6 +4329,23 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedMethodDrillRecoveryOutcomeEntr } } +void UHyperTwistCoachDashboardWidget::SyncSelectedMethodDrillRecoveryMemoryHistoryEntry() +{ + if (MethodDrillRecoveryMemoryHistoryEntries.Num() <= 0) + { + SelectedMethodDrillRecoveryMemoryHistoryIndex = INDEX_NONE; + return; + } + + if (SelectedMethodDrillRecoveryMemoryHistoryIndex < 0 + || SelectedMethodDrillRecoveryMemoryHistoryIndex + >= MethodDrillRecoveryMemoryHistoryEntries.Num()) + { + SelectedMethodDrillRecoveryMemoryHistoryIndex = + MethodDrillRecoveryMemoryHistoryEntries.Num() - 1; + } +} + void UHyperTwistCoachDashboardWidget::SyncPreferredGuidancePreviewLane(const bool bForceAdoptPreferredLane) { const FHyperTwistCoachDashboardGuidanceLanePreference Preference = BuildGuidanceLanePreference(); @@ -5705,6 +5850,115 @@ void UHyperTwistCoachDashboardWidget::RecordMethodDrillFollowUpHistoryEntry( SelectedMethodDrillFollowUpHistoryIndex = MethodDrillFollowUpHistoryEntries.Num() - 1; } +void UHyperTwistCoachDashboardWidget::RecordMethodDrillRecoveryMemoryHistorySnapshot( + const FHyperTwistCoachDashboardQueueRecoveryWeighting& QueueRecoveryWeighting, + const FString& QueueRecoveryStatusLine, + const FString& HandoffRecommendationKindLabel, + const FString& HandoffRecommendationStatusLine, + const bool bUsedMethodDrillRecoveryMemoryForHandoff +) +{ + const bool bHasMethodDrillRecoveryHistory = + CachedCoachPanelState.MethodDrillRecoveryHistoryCount > 0; + const bool bDrillRecoverySourceActive = + CachedCoachPanelState.PreferredQueueRecoveryActionSourceLabel + == TEXT("coach-memory-method-drill-recovery-strong") + || CachedCoachPanelState.PreferredQueueRecoveryActionSourceLabel + == TEXT("coach-memory-method-drill-recovery-weak"); + if (!bHasMethodDrillRecoveryHistory + && !QueueRecoveryWeighting.bHasMethodDrillMemorySignal + && !bUsedMethodDrillRecoveryMemoryForHandoff + && !bDrillRecoverySourceActive) + { + return; + } + + FHyperTwistCoachDashboardMethodDrillRecoveryMemoryHistoryEntry Entry; + Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); + Entry.PreferredRecoveryAction = CachedCoachPanelState.PreferredQueueRecoveryAction; + Entry.PreferredRecoveryActionSourceLabel = + CachedCoachPanelState.PreferredQueueRecoveryActionSourceLabel; + Entry.RecommendedHandoffKindLabel = + HandoffRecommendationKindLabel.IsEmpty() ? TEXT("none") : HandoffRecommendationKindLabel; + Entry.QueueRecoveryStatusLine = QueueRecoveryStatusLine; + Entry.HandoffRecommendationStatusLine = HandoffRecommendationStatusLine; + Entry.MethodDrillRecoveryHistoryCount = + CachedCoachPanelState.MethodDrillRecoveryHistoryCount; + Entry.StrongMethodDrillRecoveryHistoryCount = + CachedCoachPanelState.StrongMethodDrillRecoveryHistoryCount; + Entry.WeakMethodDrillRecoveryHistoryCount = + CachedCoachPanelState.WeakMethodDrillRecoveryHistoryCount; + Entry.MethodDrillRecoveryAverageOutcomeScore = + CachedCoachPanelState.MethodDrillRecoveryAverageOutcomeScore; + Entry.bStrongPressureDominant = + Entry.MethodDrillRecoveryHistoryCount >= 2 + && Entry.StrongMethodDrillRecoveryHistoryCount + >= Entry.WeakMethodDrillRecoveryHistoryCount + && Entry.MethodDrillRecoveryAverageOutcomeScore >= 55.0f; + Entry.bWeakPressureDominant = + Entry.MethodDrillRecoveryHistoryCount >= 2 + && Entry.WeakMethodDrillRecoveryHistoryCount + > Entry.StrongMethodDrillRecoveryHistoryCount + && Entry.MethodDrillRecoveryAverageOutcomeScore <= 45.0f; + Entry.bUsedMethodDrillRecoveryMemoryForRecovery = + QueueRecoveryWeighting.bHasMethodDrillMemorySignal; + Entry.bUsedMethodDrillRecoveryMemoryForHandoff = + bUsedMethodDrillRecoveryMemoryForHandoff; + if (!Entry.IsStructurallyValid()) + { + return; + } + + const bool bMatchesLastEntry = MethodDrillRecoveryMemoryHistoryEntries.Num() > 0 + && MethodDrillRecoveryMemoryHistoryEntries.Last().PreferredRecoveryAction + == Entry.PreferredRecoveryAction + && MethodDrillRecoveryMemoryHistoryEntries.Last().PreferredRecoveryActionSourceLabel + == Entry.PreferredRecoveryActionSourceLabel + && MethodDrillRecoveryMemoryHistoryEntries.Last().RecommendedHandoffKindLabel + == Entry.RecommendedHandoffKindLabel + && MethodDrillRecoveryMemoryHistoryEntries.Last().QueueRecoveryStatusLine + == Entry.QueueRecoveryStatusLine + && MethodDrillRecoveryMemoryHistoryEntries.Last().HandoffRecommendationStatusLine + == Entry.HandoffRecommendationStatusLine + && MethodDrillRecoveryMemoryHistoryEntries.Last().MethodDrillRecoveryHistoryCount + == Entry.MethodDrillRecoveryHistoryCount + && MethodDrillRecoveryMemoryHistoryEntries.Last().StrongMethodDrillRecoveryHistoryCount + == Entry.StrongMethodDrillRecoveryHistoryCount + && MethodDrillRecoveryMemoryHistoryEntries.Last().WeakMethodDrillRecoveryHistoryCount + == Entry.WeakMethodDrillRecoveryHistoryCount + && FMath::IsNearlyEqual( + MethodDrillRecoveryMemoryHistoryEntries.Last().MethodDrillRecoveryAverageOutcomeScore, + Entry.MethodDrillRecoveryAverageOutcomeScore) + && MethodDrillRecoveryMemoryHistoryEntries.Last().bStrongPressureDominant + == Entry.bStrongPressureDominant + && MethodDrillRecoveryMemoryHistoryEntries.Last().bWeakPressureDominant + == Entry.bWeakPressureDominant + && MethodDrillRecoveryMemoryHistoryEntries.Last().bUsedMethodDrillRecoveryMemoryForRecovery + == Entry.bUsedMethodDrillRecoveryMemoryForRecovery + && MethodDrillRecoveryMemoryHistoryEntries.Last().bUsedMethodDrillRecoveryMemoryForHandoff + == Entry.bUsedMethodDrillRecoveryMemoryForHandoff; + if (bMatchesLastEntry) + { + return; + } + + MethodDrillRecoveryMemoryHistoryEntries.Add(Entry); + constexpr int32 MaxRetainedMethodDrillRecoveryMemoryHistoryEntries = 16; + if (MethodDrillRecoveryMemoryHistoryEntries.Num() + > MaxRetainedMethodDrillRecoveryMemoryHistoryEntries) + { + const int32 ExcessCount = + MethodDrillRecoveryMemoryHistoryEntries.Num() + - MaxRetainedMethodDrillRecoveryMemoryHistoryEntries; + MethodDrillRecoveryMemoryHistoryEntries.RemoveAt( + 0, + ExcessCount, + EAllowShrinking::No); + } + SelectedMethodDrillRecoveryMemoryHistoryIndex = + MethodDrillRecoveryMemoryHistoryEntries.Num() - 1; +} + void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() @@ -7178,6 +7432,14 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() QueueRecoveryWeighting.MethodDrillUpstreamHistoryCount, QueueRecoveryWeighting.MethodDrillUpstreamAverageOutcomeScore ); + RecordMethodDrillRecoveryMemoryHistorySnapshot( + QueueRecoveryWeighting, + QueueRecoveryStatusLine, + HyperTwistCoachDashboardWidgetInternal::DescribeHandoffRecommendationKind( + HandoffRecommendationKind), + CoachHandoffRecommendationStatusLine, + bHandoffRecommendationUsedMethodDrillRecoveryMemory + ); const bool bHasPrimaryQueueSuppression = CachedCoachSessionQueueSummary.bSuppressedPrimaryQueueCreation && !CachedCoachSessionQueueSummary.PrimaryQueueSuppressionReason.IsEmpty(); @@ -7350,6 +7612,67 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() 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 FHyperTwistCoachDashboardMethodDrillRecoveryMemoryHistoryEntry* + SelectedMethodDrillRecoveryMemoryHistoryEntry = + SelectedMethodDrillRecoveryMemoryHistoryIndex >= 0 + && SelectedMethodDrillRecoveryMemoryHistoryIndex + < MethodDrillRecoveryMemoryHistoryEntries.Num() + ? &MethodDrillRecoveryMemoryHistoryEntries[ + SelectedMethodDrillRecoveryMemoryHistoryIndex] + : nullptr; + const FString MethodDrillRecoveryMemoryActionLabel = + SelectedMethodDrillRecoveryMemoryHistoryEntry == nullptr + || SelectedMethodDrillRecoveryMemoryHistoryEntry->PreferredRecoveryAction + == EHyperTwistTrainingQueueRecoveryAction::None + ? TEXT("none") + : HyperTwistCoachDashboardWidgetInternal::EnumDisplayName( + SelectedMethodDrillRecoveryMemoryHistoryEntry->PreferredRecoveryAction); + const FString MethodDrillRecoveryMemorySourceLine = + SelectedMethodDrillRecoveryMemoryHistoryEntry == nullptr + || SelectedMethodDrillRecoveryMemoryHistoryEntry->PreferredRecoveryActionSourceLabel.IsEmpty() + ? TEXT("n/a") + : HyperTwistCoachDashboardWidgetInternal::DescribeCoachMemorySourceLabel( + SelectedMethodDrillRecoveryMemoryHistoryEntry->PreferredRecoveryActionSourceLabel); + const FString MethodDrillRecoveryMemoryDominantPressureLine = + SelectedMethodDrillRecoveryMemoryHistoryEntry == nullptr + ? TEXT("n/a") + : (SelectedMethodDrillRecoveryMemoryHistoryEntry->bStrongPressureDominant + ? TEXT("strong") + : (SelectedMethodDrillRecoveryMemoryHistoryEntry->bWeakPressureDominant + ? TEXT("weak") + : TEXT("balanced"))); + const FString MethodDrillRecoveryMemoryHistoryStatusLine = + SelectedMethodDrillRecoveryMemoryHistoryEntry != nullptr + && SelectedMethodDrillRecoveryMemoryHistoryEntry->IsStructurallyValid() + ? FString::Printf( + TEXT("Drill recovery memory history: %d entries | selected %d/%d | %s | %s | Recorded: %s"), + MethodDrillRecoveryMemoryHistoryEntries.Num(), + SelectedMethodDrillRecoveryMemoryHistoryIndex + 1, + MethodDrillRecoveryMemoryHistoryEntries.Num(), + *MethodDrillRecoveryMemoryActionLabel, + *SelectedMethodDrillRecoveryMemoryHistoryEntry->RecommendedHandoffKindLabel, + *SelectedMethodDrillRecoveryMemoryHistoryEntry->RecordedAtUtc) + : TEXT("Drill recovery memory history: no retained upstream drill-recovery memory shifts recorded yet."); + const FString MethodDrillRecoveryMemoryHistoryDetailLine = + SelectedMethodDrillRecoveryMemoryHistoryEntry != nullptr + && SelectedMethodDrillRecoveryMemoryHistoryEntry->IsStructurallyValid() + ? FString::Printf( + TEXT("Source: %s | Strong/weak/history %d/%d/%d avg %.1f | Dominant pressure: %s | Recovery memory tipped: %s | Handoff memory tipped: %s | Recovery posture: %s | Handoff posture: %s"), + *MethodDrillRecoveryMemorySourceLine, + SelectedMethodDrillRecoveryMemoryHistoryEntry->StrongMethodDrillRecoveryHistoryCount, + SelectedMethodDrillRecoveryMemoryHistoryEntry->WeakMethodDrillRecoveryHistoryCount, + SelectedMethodDrillRecoveryMemoryHistoryEntry->MethodDrillRecoveryHistoryCount, + SelectedMethodDrillRecoveryMemoryHistoryEntry->MethodDrillRecoveryAverageOutcomeScore, + *MethodDrillRecoveryMemoryDominantPressureLine, + SelectedMethodDrillRecoveryMemoryHistoryEntry->bUsedMethodDrillRecoveryMemoryForRecovery + ? TEXT("yes") + : TEXT("no"), + SelectedMethodDrillRecoveryMemoryHistoryEntry->bUsedMethodDrillRecoveryMemoryForHandoff + ? TEXT("yes") + : TEXT("no"), + *SelectedMethodDrillRecoveryMemoryHistoryEntry->QueueRecoveryStatusLine, + *SelectedMethodDrillRecoveryMemoryHistoryEntry->HandoffRecommendationStatusLine) + : TEXT("Drill recovery memory detail: retained upstream strong-vs-weak drill-recovery pressure, resulting recovery posture, and resulting handoff posture will accumulate here for later comparison."); const FString PreferredDeferredRecoveryEntryId = FindFirstDeferredQueueEntryId(); const bool bCanApplyQueueRecovery = !bHasLiveAttemptTimer @@ -8780,6 +9103,41 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() NextMethodDrillRecoveryHistoryButton->SetIsEnabled( MethodDrillRecoveryHistoryIndices.Num() > 1); } + if (MethodDrillRecoveryMemoryHistoryHeaderTextBlock != nullptr) + { + MethodDrillRecoveryMemoryHistoryHeaderTextBlock->SetText(FText::FromString( + TEXT("[Drill Recovery Memory History]"))); + } + if (MethodDrillRecoveryMemoryHistoryStatusTextBlock != nullptr) + { + MethodDrillRecoveryMemoryHistoryStatusTextBlock->SetText(FText::FromString( + MethodDrillRecoveryMemoryHistoryStatusLine)); + } + if (MethodDrillRecoveryMemoryHistoryDetailTextBlock != nullptr) + { + MethodDrillRecoveryMemoryHistoryDetailTextBlock->SetText(FText::FromString( + MethodDrillRecoveryMemoryHistoryDetailLine)); + } + if (PreviousMethodDrillRecoveryMemoryHistoryButtonLabel != nullptr) + { + PreviousMethodDrillRecoveryMemoryHistoryButtonLabel->SetText(FText::FromString( + TEXT("Prev Drill Memory"))); + } + if (PreviousMethodDrillRecoveryMemoryHistoryButton != nullptr) + { + PreviousMethodDrillRecoveryMemoryHistoryButton->SetIsEnabled( + MethodDrillRecoveryMemoryHistoryEntries.Num() > 1); + } + if (NextMethodDrillRecoveryMemoryHistoryButtonLabel != nullptr) + { + NextMethodDrillRecoveryMemoryHistoryButtonLabel->SetText(FText::FromString( + TEXT("Next Drill Memory"))); + } + if (NextMethodDrillRecoveryMemoryHistoryButton != nullptr) + { + NextMethodDrillRecoveryMemoryHistoryButton->SetIsEnabled( + MethodDrillRecoveryMemoryHistoryEntries.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 5dc142b..7f35f18 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -320,6 +320,32 @@ struct FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry } }; +struct FHyperTwistCoachDashboardMethodDrillRecoveryMemoryHistoryEntry +{ + FString RecordedAtUtc; + EHyperTwistTrainingQueueRecoveryAction PreferredRecoveryAction = + EHyperTwistTrainingQueueRecoveryAction::None; + FString PreferredRecoveryActionSourceLabel; + FString RecommendedHandoffKindLabel; + FString QueueRecoveryStatusLine; + FString HandoffRecommendationStatusLine; + int32 MethodDrillRecoveryHistoryCount = 0; + int32 StrongMethodDrillRecoveryHistoryCount = 0; + int32 WeakMethodDrillRecoveryHistoryCount = 0; + float MethodDrillRecoveryAverageOutcomeScore = 0.0f; + bool bStrongPressureDominant = false; + bool bWeakPressureDominant = false; + bool bUsedMethodDrillRecoveryMemoryForRecovery = false; + bool bUsedMethodDrillRecoveryMemoryForHandoff = false; + + bool IsStructurallyValid() const + { + return !RecordedAtUtc.IsEmpty() + && MethodDrillRecoveryHistoryCount > 0 + && (!QueueRecoveryStatusLine.IsEmpty() || !HandoffRecommendationStatusLine.IsEmpty()); + } +}; + struct FHyperTwistCoachDashboardMethodDrillFollowUpWeighting { int32 MatchingHistoryCount = 0; @@ -395,6 +421,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") int32 SelectedMethodDrillRecoveryOutcomeIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") + int32 SelectedMethodDrillRecoveryMemoryHistoryIndex = INDEX_NONE; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") bool bHasLiveAttemptTimer = false; @@ -512,6 +541,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectNextMethodDrillRecoveryOutcomeEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectPreviousMethodDrillRecoveryMemoryHistoryEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectNextMethodDrillRecoveryMemoryHistoryEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool StartLiveAttemptTimer(); @@ -914,6 +949,27 @@ protected: UPROPERTY(Transient) TObjectPtr NextMethodDrillRecoveryHistoryButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr MethodDrillRecoveryMemoryHistoryHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr MethodDrillRecoveryMemoryHistoryStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr MethodDrillRecoveryMemoryHistoryDetailTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousMethodDrillRecoveryMemoryHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousMethodDrillRecoveryMemoryHistoryButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextMethodDrillRecoveryMemoryHistoryButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextMethodDrillRecoveryMemoryHistoryButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr QueueSuppressionHistoryHeaderTextBlock = nullptr; @@ -1193,6 +1249,12 @@ protected: UFUNCTION() void HandleNextMethodDrillRecoveryHistoryClicked(); + UFUNCTION() + void HandlePreviousMethodDrillRecoveryMemoryHistoryClicked(); + + UFUNCTION() + void HandleNextMethodDrillRecoveryMemoryHistoryClicked(); + UFUNCTION() void HandleClearRunClicked(); @@ -1253,6 +1315,7 @@ private: void SyncSelectedClosureRecoveryHistoryEntry(); void SyncSelectedMethodDrillFollowUpHistoryEntry(); void SyncSelectedMethodDrillRecoveryOutcomeEntry(); + void SyncSelectedMethodDrillRecoveryMemoryHistoryEntry(); void SyncPreferredGuidancePreviewLane(bool bForceAdoptPreferredLane); void UpdateDashboardPresentation(); void RefreshLiveAttemptClock(); @@ -1313,6 +1376,13 @@ private: void RecordQueueSuppressionSnapshot(); void RecordClosureRecoverySnapshot(); void RecordMethodDrillFollowUpHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap); + void RecordMethodDrillRecoveryMemoryHistorySnapshot( + const FHyperTwistCoachDashboardQueueRecoveryWeighting& QueueRecoveryWeighting, + const FString& QueueRecoveryStatusLine, + const FString& HandoffRecommendationKindLabel, + const FString& HandoffRecommendationStatusLine, + bool bUsedMethodDrillRecoveryMemoryForHandoff + ); void RecordGuidanceOutcomeDecision( EHyperTwistCoachDashboardOutcomeAction RecommendedAction, EHyperTwistCoachDashboardOutcomeAction AppliedAction, @@ -1367,6 +1437,8 @@ private: TArray GuidanceDecisionHistoryEntries; TArray HandoffHistoryEntries; TArray MethodDrillFollowUpHistoryEntries; + TArray + MethodDrillRecoveryMemoryHistoryEntries; bool bHasQueueRecoveryOutcome = false; FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome; TArray QueueRecoveryOutcomeHistory;