Add live timer loop to coach dashboard

This commit is contained in:
axiomlogicnexus 2026-04-27 06:13:35 +02:00
parent baccddc0a8
commit 1d72a8e7a5
2 changed files with 484 additions and 37 deletions

View file

@ -9,6 +9,7 @@
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "HAL/PlatformTime.h"
namespace HyperTwistCoachDashboardWidgetInternal
{
@ -107,9 +108,37 @@ void UHyperTwistCoachDashboardWidget::NativeConstruct()
RefreshCoachDashboardView();
}
void UHyperTwistCoachDashboardWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
if (bRefreshLiveAttemptClockOnTick && bHasLiveAttemptTimer)
{
RefreshLiveAttemptClock();
UpdateDashboardPresentation();
}
}
void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView()
{
RefreshTrainingState();
if (bHasLiveAttemptTimer
&& (!CachedRunState.Session.IsStructurallyValid()
|| !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid()))
{
bHasLiveAttemptTimer = false;
LiveAttemptPhase = EHyperTwistCoachDashboardAttemptPhase::Idle;
LiveAttemptStartedAtSeconds = 0.0;
LiveAttemptSolveStartedAtSeconds = 0.0;
LiveAttemptStoredInspectionElapsedMs = 0;
LiveAttemptInspectionElapsedMs = 0;
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = 0;
}
else
{
RefreshLiveAttemptClock();
}
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
@ -264,6 +293,113 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitSyntheti
return SubmitTrainingAttempt(Attempt, TimeMs);
}
bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer()
{
if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
{
return false;
}
if (bHasLiveAttemptTimer)
{
return AdvanceLiveAttemptToSolvePhase();
}
const FHyperTwistTrainingTimingPolicy* TimingPolicy = FindActiveTimingPolicy();
const bool bUseInspection = TimingPolicy != nullptr
&& TimingPolicy->bInspectionEnabled
&& TimingPolicy->InspectionDurationMs > 0;
bHasLiveAttemptTimer = true;
LiveAttemptPhase = bUseInspection
? EHyperTwistCoachDashboardAttemptPhase::Inspection
: EHyperTwistCoachDashboardAttemptPhase::Solving;
LiveAttemptStartedAtSeconds = FPlatformTime::Seconds();
LiveAttemptSolveStartedAtSeconds = bUseInspection ? 0.0 : LiveAttemptStartedAtSeconds;
LiveAttemptStoredInspectionElapsedMs = 0;
LiveAttemptInspectionElapsedMs = 0;
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = 0;
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::AdvanceLiveAttemptToSolvePhase()
{
if (!bHasLiveAttemptTimer || LiveAttemptPhase != EHyperTwistCoachDashboardAttemptPhase::Inspection)
{
return false;
}
RefreshLiveAttemptClock();
LiveAttemptStoredInspectionElapsedMs = LiveAttemptInspectionElapsedMs;
LiveAttemptSolveStartedAtSeconds = FPlatformTime::Seconds();
LiveAttemptPhase = EHyperTwistCoachDashboardAttemptPhase::Solving;
UpdateDashboardPresentation();
return true;
}
void UHyperTwistCoachDashboardWidget::CancelLiveAttemptTimer()
{
bHasLiveAttemptTimer = false;
LiveAttemptPhase = EHyperTwistCoachDashboardAttemptPhase::Idle;
LiveAttemptStartedAtSeconds = 0.0;
LiveAttemptSolveStartedAtSeconds = 0.0;
LiveAttemptStoredInspectionElapsedMs = 0;
LiveAttemptInspectionElapsedMs = 0;
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = 0;
UpdateDashboardPresentation();
}
FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitLiveTimedAttempt(
const EHyperTwistTrainingAttemptResult Result
)
{
if (!bHasLiveAttemptTimer
|| LiveAttemptPhase != EHyperTwistCoachDashboardAttemptPhase::Solving
|| !CachedRunState.Session.IsStructurallyValid()
|| !CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid())
{
return FHyperTwistTrainingRunStepResult();
}
RefreshLiveAttemptClock();
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(LiveAttemptSolveElapsedMs, 0);
Attempt.ExecutionTimeMs = FMath::Max(LiveAttemptSolveElapsedMs, 0);
Attempt.TimingBreakdown.InspectionElapsedMs = FMath::Max(LiveAttemptInspectionElapsedMs, 0);
Attempt.TimingBreakdown.RawSolveTimeMs = FMath::Max(LiveAttemptSolveElapsedMs, 0);
Attempt.TimingBreakdown.FinalTimeMs = FMath::Max(LiveAttemptSolveElapsedMs, 0);
Attempt.TimingBreakdown.bDidNotFinish =
Result == EHyperTwistTrainingAttemptResult::Timeout
|| Result == EHyperTwistTrainingAttemptResult::DNF
|| Result == EHyperTwistTrainingAttemptResult::Aborted;
if (Result == EHyperTwistTrainingAttemptResult::DNF)
{
Attempt.TimingBreakdown.ManualPenalty = EHyperTwistTrainingPenalty::DNF;
Attempt.TimingBreakdown.AppliedPenalty = EHyperTwistTrainingPenalty::DNF;
}
Attempt.ReplayId = CachedRunState.ReplayPacket.ReplayId;
Attempt.CompletedAtUtc = FDateTime::UtcNow().ToIso8601();
const FHyperTwistTrainingRunStepResult StepResult = SubmitTrainingAttempt(
Attempt,
FMath::Max(LiveAttemptSolveElapsedMs, 0)
);
CancelLiveAttemptTimer();
return StepResult;
}
void UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked()
{
ExecutePrimaryCoachAction();
@ -315,40 +451,68 @@ void UHyperTwistCoachDashboardWidget::HandleClearRunClicked()
void UHyperTwistCoachDashboardWidget::HandleRecordSuccessClicked()
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Success,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Success)
);
if (bHasLiveAttemptTimer)
{
SubmitLiveTimedAttempt(EHyperTwistTrainingAttemptResult::Success);
}
else
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Success,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Success)
);
}
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleRecordFailureClicked()
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Failure,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Failure)
);
if (bHasLiveAttemptTimer)
{
SubmitLiveTimedAttempt(EHyperTwistTrainingAttemptResult::Failure);
}
else
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Failure,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Failure)
);
}
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleRecordTimeoutClicked()
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Timeout,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Timeout)
);
if (bHasLiveAttemptTimer)
{
SubmitLiveTimedAttempt(EHyperTwistTrainingAttemptResult::Timeout);
}
else
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::Timeout,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Timeout)
);
}
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleRecordDnfClicked()
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::DNF,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::DNF)
);
if (bHasLiveAttemptTimer)
{
SubmitLiveTimedAttempt(EHyperTwistTrainingAttemptResult::DNF);
}
else
{
SubmitSyntheticCurrentAttempt(
EHyperTwistTrainingAttemptResult::DNF,
ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::DNF)
);
}
SyncSelectedQueueEntry();
UpdateDashboardPresentation();
}
@ -372,6 +536,23 @@ void UHyperTwistCoachDashboardWidget::HandleUseAverageTimeClicked()
UpdateDashboardPresentation();
}
void UHyperTwistCoachDashboardWidget::HandleStartTimerClicked()
{
if (bHasLiveAttemptTimer && IsLiveAttemptInspectionPhase())
{
AdvanceLiveAttemptToSolvePhase();
}
else
{
StartLiveAttemptTimer();
}
}
void UHyperTwistCoachDashboardWidget::HandleCancelTimerClicked()
{
CancelLiveAttemptTimer();
}
void UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked()
{
SelectPreviousQueueEntry();
@ -472,6 +653,18 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
WidgetTree,
RootLayout,
TEXT("CoachAttemptInputStatus"),
2
);
TimerHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachTimerHeader"),
2
);
TimerStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachTimerStatus"),
8
);
FollowUpTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
@ -647,6 +840,40 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
UseAverageTimeButtonLabel = UseAverageTimeLabelRaw;
}
UHorizontalBox* LiveTimerRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardLiveTimerRow")
);
if (LiveTimerRow != nullptr)
{
if (UVerticalBoxSlot* LiveTimerRowSlot = RootLayout->AddChildToVerticalBox(LiveTimerRow))
{
LiveTimerRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f));
}
UTextBlock* StartTimerLabelRaw = nullptr;
StartTimerButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
LiveTimerRow,
TEXT("StartTimerButton"),
TEXT("StartTimerButtonLabel"),
TEXT("Start Attempt"),
StartTimerLabelRaw
);
StartTimerButtonLabel = StartTimerLabelRaw;
UTextBlock* CancelTimerLabelRaw = nullptr;
CancelTimerButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
LiveTimerRow,
TEXT("CancelTimerButton"),
TEXT("CancelTimerButtonLabel"),
TEXT("Cancel Timer"),
CancelTimerLabelRaw
);
CancelTimerButtonLabel = CancelTimerLabelRaw;
}
UHorizontalBox* RunControlRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardRunControlButtons")
@ -818,6 +1045,14 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
UseAverageTimeButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleUseAverageTimeClicked);
}
if (StartTimerButton != nullptr)
{
StartTimerButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleStartTimerClicked);
}
if (CancelTimerButton != nullptr)
{
CancelTimerButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCancelTimerClicked);
}
if (PreviousQueueButton != nullptr)
{
PreviousQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked);
@ -904,6 +1139,29 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
const int32 ResolvedFailureTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Failure);
const int32 ResolvedTimeoutTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Timeout);
const int32 ResolvedDnfTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::DNF);
const FHyperTwistTrainingTimingPolicy* TimingPolicy = FindActiveTimingPolicy();
const bool bHasInspectionWindow = TimingPolicy != nullptr
&& TimingPolicy->bInspectionEnabled
&& TimingPolicy->InspectionDurationMs > 0;
const int32 InspectionLimitMs = bHasInspectionWindow ? FMath::Max(TimingPolicy->InspectionDurationMs, 0) : 0;
const bool bLiveInspectionPhase = IsLiveAttemptInspectionPhase();
const bool bLiveSolvePhase = bHasLiveAttemptTimer
&& LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Solving;
const FString RunningSolveLabel =
TimingPolicy != nullptr && TimingPolicy->bHideRunningTime && bLiveSolvePhase
? TEXT("hidden")
: FString::FromInt(LiveAttemptSolveElapsedMs);
const EHyperTwistTrainingPenalty LiveInspectionPenalty =
(bHasInspectionWindow
&& TimingPolicy->bInspectionPenaltiesEnabled
&& LiveAttemptInspectionElapsedMs > InspectionLimitMs
&& LiveAttemptInspectionElapsedMs <= InspectionLimitMs + 2000)
? EHyperTwistTrainingPenalty::Plus2
: ((bHasInspectionWindow
&& TimingPolicy->bInspectionPenaltiesEnabled
&& LiveAttemptInspectionElapsedMs > InspectionLimitMs + 2000)
? EHyperTwistTrainingPenalty::DNF
: EHyperTwistTrainingPenalty::None);
const FString VerificationNotePreview = CachedRepositoryRoundTripVerification.VerificationNotes.Num() > 0
? CachedRepositoryRoundTripVerification.VerificationNotes[0]
: TEXT("none");
@ -1042,13 +1300,50 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
ResolvedDnfTimeMs
)));
}
if (TimerHeaderTextBlock != nullptr)
{
TimerHeaderTextBlock->SetText(FText::FromString(TEXT("[Live Timer]")));
}
if (TimerStatusTextBlock != nullptr)
{
if (bLiveInspectionPhase)
{
const int32 RemainingInspectionMs = bHasInspectionWindow
? FMath::Max(InspectionLimitMs - LiveAttemptInspectionElapsedMs, 0)
: 0;
TimerStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Phase: inspection | elapsed %d ms | remaining %d ms | pending penalty: %s | click Start Solve when ready"),
LiveAttemptInspectionElapsedMs,
RemainingInspectionMs,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(LiveInspectionPenalty)
)));
}
else if (bLiveSolvePhase)
{
TimerStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Phase: solving | inspection %d ms | solve %s ms | finish with Success / Failure / Timeout / DNF"),
LiveAttemptInspectionElapsedMs,
*RunningSolveLabel
)));
}
else
{
const FString InspectionLabel = bHasInspectionWindow
? FString::Printf(TEXT("inspection target %d ms"), InspectionLimitMs)
: TEXT("no inspection window");
TimerStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Timer idle | %s | start a live attempt for less-synthetic run entry"),
*InspectionLabel
)));
}
}
if (AttemptTimeTextBox != nullptr)
{
const FString HintLabel = CurrentTrainingCase.TimeTargetMs > 0
? FString::Printf(TEXT("Enter attempt ms (target %d)"), CurrentTrainingCase.TimeTargetMs)
: TEXT("Enter attempt ms");
AttemptTimeTextBox->SetHintText(FText::FromString(HintLabel));
AttemptTimeTextBox->SetIsEnabled(bHasRunnableCurrentCase);
AttemptTimeTextBox->SetIsEnabled(!bHasLiveAttemptTimer && bHasRunnableCurrentCase);
}
if (FollowUpTextBlock != nullptr)
{
@ -1237,9 +1532,12 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
if (PrimaryActionButton != nullptr)
{
PrimaryActionButton->SetIsEnabled(
CachedCoachPanelState.bCanStartQueuedRun
|| CachedCoachPanelState.bCanStartCoachFollowUpRun
|| CachedCoachPanelState.bCanStartCoachRecommendedRun
!bHasLiveAttemptTimer
&& (
CachedCoachPanelState.bCanStartQueuedRun
|| CachedCoachPanelState.bCanStartCoachFollowUpRun
|| CachedCoachPanelState.bCanStartCoachRecommendedRun
)
);
}
@ -1253,7 +1551,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (DeferButton != nullptr)
{
DeferButton->SetIsEnabled(FindSelectedQueueEntry() != nullptr);
DeferButton->SetIsEnabled(!bHasLiveAttemptTimer && FindSelectedQueueEntry() != nullptr);
}
if (PromoteButtonLabel != nullptr)
{
@ -1261,7 +1559,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (PromoteButton != nullptr)
{
PromoteButton->SetIsEnabled(!FindFirstDeferredQueueEntryId().IsEmpty());
PromoteButton->SetIsEnabled(!bHasLiveAttemptTimer && !FindFirstDeferredQueueEntryId().IsEmpty());
}
if (CompleteRunButtonLabel != nullptr)
{
@ -1269,7 +1567,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (CompleteRunButton != nullptr)
{
CompleteRunButton->SetIsEnabled(bHasActiveRun);
CompleteRunButton->SetIsEnabled(bHasActiveRun && !bHasLiveAttemptTimer);
}
if (ClearRunButtonLabel != nullptr)
{
@ -1277,39 +1575,47 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (ClearRunButton != nullptr)
{
ClearRunButton->SetIsEnabled(bHasActiveRun);
ClearRunButton->SetIsEnabled(bHasActiveRun && !bHasLiveAttemptTimer);
}
if (RecordSuccessButtonLabel != nullptr)
{
RecordSuccessButtonLabel->SetText(FText::FromString(TEXT("Record Success")));
RecordSuccessButtonLabel->SetText(FText::FromString(
bLiveSolvePhase ? TEXT("Stop Success") : TEXT("Record Success")
));
}
if (RecordSuccessButton != nullptr)
{
RecordSuccessButton->SetIsEnabled(bHasRunnableCurrentCase);
RecordSuccessButton->SetIsEnabled(bLiveSolvePhase || (!bHasLiveAttemptTimer && bHasRunnableCurrentCase));
}
if (RecordFailureButtonLabel != nullptr)
{
RecordFailureButtonLabel->SetText(FText::FromString(TEXT("Record Failure")));
RecordFailureButtonLabel->SetText(FText::FromString(
bLiveSolvePhase ? TEXT("Stop Failure") : TEXT("Record Failure")
));
}
if (RecordFailureButton != nullptr)
{
RecordFailureButton->SetIsEnabled(bHasRunnableCurrentCase);
RecordFailureButton->SetIsEnabled(bLiveSolvePhase || (!bHasLiveAttemptTimer && bHasRunnableCurrentCase));
}
if (RecordTimeoutButtonLabel != nullptr)
{
RecordTimeoutButtonLabel->SetText(FText::FromString(TEXT("Record Timeout")));
RecordTimeoutButtonLabel->SetText(FText::FromString(
bLiveSolvePhase ? TEXT("Stop Timeout") : TEXT("Record Timeout")
));
}
if (RecordTimeoutButton != nullptr)
{
RecordTimeoutButton->SetIsEnabled(bHasRunnableCurrentCase);
RecordTimeoutButton->SetIsEnabled(bLiveSolvePhase || (!bHasLiveAttemptTimer && bHasRunnableCurrentCase));
}
if (RecordDnfButtonLabel != nullptr)
{
RecordDnfButtonLabel->SetText(FText::FromString(TEXT("Record DNF")));
RecordDnfButtonLabel->SetText(FText::FromString(
bLiveSolvePhase ? TEXT("Stop DNF") : TEXT("Record DNF")
));
}
if (RecordDnfButton != nullptr)
{
RecordDnfButton->SetIsEnabled(bHasRunnableCurrentCase);
RecordDnfButton->SetIsEnabled(bLiveSolvePhase || (!bHasLiveAttemptTimer && bHasRunnableCurrentCase));
}
if (UseTargetTimeButtonLabel != nullptr)
{
@ -1317,7 +1623,9 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (UseTargetTimeButton != nullptr)
{
UseTargetTimeButton->SetIsEnabled(bHasRunnableCurrentCase && CurrentTrainingCase.TimeTargetMs > 0);
UseTargetTimeButton->SetIsEnabled(
!bHasLiveAttemptTimer && bHasRunnableCurrentCase && CurrentTrainingCase.TimeTargetMs > 0
);
}
if (UseAverageTimeButtonLabel != nullptr)
{
@ -1325,7 +1633,31 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (UseAverageTimeButton != nullptr)
{
UseAverageTimeButton->SetIsEnabled(bHasActiveRun && CachedRunSummary.AverageTotalTimeMs > 0);
UseAverageTimeButton->SetIsEnabled(
!bHasLiveAttemptTimer && bHasActiveRun && CachedRunSummary.AverageTotalTimeMs > 0
);
}
if (StartTimerButtonLabel != nullptr)
{
StartTimerButtonLabel->SetText(FText::FromString(
bLiveInspectionPhase ? TEXT("Start Solve")
: (bHasLiveAttemptTimer ? TEXT("Timer Active") : TEXT("Start Attempt"))
));
}
if (StartTimerButton != nullptr)
{
StartTimerButton->SetIsEnabled(
bHasRunnableCurrentCase
&& (!bHasLiveAttemptTimer || bLiveInspectionPhase)
);
}
if (CancelTimerButtonLabel != nullptr)
{
CancelTimerButtonLabel->SetText(FText::FromString(TEXT("Cancel Timer")));
}
if (CancelTimerButton != nullptr)
{
CancelTimerButton->SetIsEnabled(bHasLiveAttemptTimer);
}
if (PreviousQueueButtonLabel != nullptr)
{
@ -1333,7 +1665,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (PreviousQueueButton != nullptr)
{
PreviousQueueButton->SetIsEnabled(CachedCoachSessionQueueState.Entries.Num() > 1);
PreviousQueueButton->SetIsEnabled(!bHasLiveAttemptTimer && CachedCoachSessionQueueState.Entries.Num() > 1);
}
if (StartSelectedButtonLabel != nullptr)
{
@ -1346,7 +1678,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
&& (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready
|| (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending
&& SelectedEntry->bCanStartImmediately));
StartSelectedButton->SetIsEnabled(bSelectedRunnable);
StartSelectedButton->SetIsEnabled(!bHasLiveAttemptTimer && bSelectedRunnable);
}
if (NextQueueButtonLabel != nullptr)
{
@ -1354,7 +1686,38 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
}
if (NextQueueButton != nullptr)
{
NextQueueButton->SetIsEnabled(CachedCoachSessionQueueState.Entries.Num() > 1);
NextQueueButton->SetIsEnabled(!bHasLiveAttemptTimer && CachedCoachSessionQueueState.Entries.Num() > 1);
}
}
void UHyperTwistCoachDashboardWidget::RefreshLiveAttemptClock()
{
if (!bHasLiveAttemptTimer)
{
LiveAttemptInspectionElapsedMs = 0;
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = 0;
return;
}
const double NowSeconds = FPlatformTime::Seconds();
if (LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Inspection)
{
LiveAttemptInspectionElapsedMs = FMath::Max(
0,
static_cast<int32>((NowSeconds - LiveAttemptStartedAtSeconds) * 1000.0)
);
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = LiveAttemptInspectionElapsedMs;
}
else if (LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Solving)
{
LiveAttemptInspectionElapsedMs = FMath::Max(LiveAttemptStoredInspectionElapsedMs, 0);
LiveAttemptSolveElapsedMs = FMath::Max(
0,
static_cast<int32>((NowSeconds - LiveAttemptSolveStartedAtSeconds) * 1000.0)
);
LiveAttemptDisplayedMs = LiveAttemptSolveElapsedMs;
}
}
@ -1473,3 +1836,18 @@ const FHyperTwistTrainingCoachSessionQueueStateEntry* UHyperTwistCoachDashboardW
return &CachedCoachSessionQueueState.Entries[SelectedQueueEntryIndex];
}
const FHyperTwistTrainingTimingPolicy* UHyperTwistCoachDashboardWidget::FindActiveTimingPolicy() const
{
if (!CachedRunState.Session.IsStructurallyValid() || !CachedRunState.ActiveDeck.IsStructurallyValid())
{
return nullptr;
}
return &CachedRunState.ActiveDeck.TimingPolicy;
}
bool UHyperTwistCoachDashboardWidget::IsLiveAttemptInspectionPhase() const
{
return bHasLiveAttemptTimer && LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Inspection;
}

View file

@ -9,6 +9,13 @@ class UEditableTextBox;
class UTextBlock;
class UVerticalBox;
enum class EHyperTwistCoachDashboardAttemptPhase : uint8
{
Idle,
Inspection,
Solving
};
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget
{
@ -16,6 +23,7 @@ class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistT
public:
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
int32 DefaultDeferredHours = 24;
@ -35,9 +43,24 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
bool bPreferActiveQueueSelectionOnRefresh = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard")
bool bRefreshLiveAttemptClockOnTick = true;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 SelectedQueueEntryIndex = INDEX_NONE;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
bool bHasLiveAttemptTimer = false;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 LiveAttemptInspectionElapsedMs = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 LiveAttemptSolveElapsedMs = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 LiveAttemptDisplayedMs = 0;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
void RefreshCoachDashboardView();
@ -65,6 +88,20 @@ public:
int32 TimeMs
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool StartLiveAttemptTimer();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool AdvanceLiveAttemptToSolvePhase();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
void CancelLiveAttemptTimer();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
FHyperTwistTrainingRunStepResult SubmitLiveTimedAttempt(
EHyperTwistTrainingAttemptResult Result
);
protected:
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> RootLayout = nullptr;
@ -105,6 +142,12 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> AttemptInputStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TimerHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TimerStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> AttemptTimeTextBox = nullptr;
@ -216,6 +259,18 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> UseAverageTimeButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> StartTimerButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StartTimerButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> CancelTimerButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CancelTimerButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousQueueButton = nullptr;
@ -270,6 +325,12 @@ protected:
UFUNCTION()
void HandleUseAverageTimeClicked();
UFUNCTION()
void HandleStartTimerClicked();
UFUNCTION()
void HandleCancelTimerClicked();
UFUNCTION()
void HandlePreviousQueueClicked();
@ -283,10 +344,18 @@ private:
void EnsureDefaultDashboardBuilt();
void SyncSelectedQueueEntry();
void UpdateDashboardPresentation();
void RefreshLiveAttemptClock();
FString BuildDefaultDeferredUntilUtc() const;
FString FindFirstDeferredQueueEntryId() const;
bool TryParseAttemptTimeOverride(int32& OutTimeMs) const;
int32 ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult Result) const;
void SetAttemptTimeText(int32 TimeMs);
const FHyperTwistTrainingCoachSessionQueueStateEntry* FindSelectedQueueEntry() const;
const FHyperTwistTrainingTimingPolicy* FindActiveTimingPolicy() const;
bool IsLiveAttemptInspectionPhase() const;
EHyperTwistCoachDashboardAttemptPhase LiveAttemptPhase = EHyperTwistCoachDashboardAttemptPhase::Idle;
double LiveAttemptStartedAtSeconds = 0.0;
double LiveAttemptSolveStartedAtSeconds = 0.0;
int32 LiveAttemptStoredInspectionElapsedMs = 0;
};