Retain drill recovery history on dashboard

This commit is contained in:
axiomlogicnexus 2026-04-29 04:09:36 +02:00
parent bcb69d171d
commit ad3e9ad19d
2 changed files with 312 additions and 0 deletions

View file

@ -760,6 +760,21 @@ namespace HyperTwistCoachDashboardWidgetInternal
+ Bias.MethodDrillFollowUpRecommendationCount * 2
+ Bias.MethodDrillRecommendedRecommendationCount;
}
void CollectMethodDrillRecoveryOutcomeIndices(
const TArray<FHyperTwistCoachDashboardQueueRecoveryOutcome>& Outcomes,
TArray<int32>& 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<int32> 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<int32> 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>(
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>(
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<int32> 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<int32> 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]")));

View file

@ -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<UTextBlock> QueueRecoveryOutcomeDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> MethodDrillRecoveryHistoryHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> MethodDrillRecoveryHistoryStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> MethodDrillRecoveryHistoryDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousMethodDrillRecoveryHistoryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PreviousMethodDrillRecoveryHistoryButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextMethodDrillRecoveryHistoryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> NextMethodDrillRecoveryHistoryButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> 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();