diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index d0aecf5..319045b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -538,6 +538,100 @@ FHyperTwistTrainingDrillCaseModeCompatibility UHyperTwistContractLibrary::MakeSa ); } +FHyperTwistTrainingMethodDrillDiagnosticPacket UHyperTwistContractLibrary::MakeSampleMethodDrillDiagnosticPacket() +{ + const FHyperTwistTrainingMethodDrillDefinition DrillDefinition = MakeSampleMethodDrillDefinition(); + if (!DrillDefinition.IsStructurallyValid()) + { + return FHyperTwistTrainingMethodDrillDiagnosticPacket(); + } + + FHyperTwistTrainingMethodDrillRunState RunState = UHyperTwistTrainingLibrary::StartMethodDrillRun( + DrillDefinition, + TEXT("local-user"), + TEXT("method_drill_run_diagnostics_01") + ); + if (!RunState.IsStructurallyValid()) + { + return FHyperTwistTrainingMethodDrillDiagnosticPacket(); + } + + RunState.StartedAtUtc = TEXT("2026-04-29T09:00:00Z"); + RunState.LastUpdatedAtUtc = RunState.StartedAtUtc; + RunState = UHyperTwistTrainingLibrary::StartNextMethodDrillCase(RunState); + if (!RunState.IsStructurallyValid()) + { + return FHyperTwistTrainingMethodDrillDiagnosticPacket(); + } + + RunState = UHyperTwistTrainingLibrary::RevealMethodDrillCase(RunState); + + FHyperTwistTrainingMethodDrillResponseRecord AssistedResponse; + AssistedResponse.ResponseId = TEXT("method_drill_diag_response_01"); + AssistedResponse.DrillRunId = RunState.RunId; + AssistedResponse.CaseId = RunState.CurrentCase.CaseId; + AssistedResponse.PresentedAtUtc = TEXT("2026-04-29T09:00:05Z"); + AssistedResponse.RespondedAtUtc = TEXT("2026-04-29T09:00:17Z"); + AssistedResponse.DrillMode = RunState.DrillDefinition.DrillMode; + AssistedResponse.VisibilityPolicy = RunState.DrillDefinition.VisibilityPolicy; + AssistedResponse.bAnswerWasRevealed = true; + AssistedResponse.bMarkedCorrect = true; + AssistedResponse.bWasRepeat = false; + AssistedResponse.ElapsedMs = 1700; + AssistedResponse.ResponseText = TEXT("recognized left block after reveal"); + RunState.Responses.Add(AssistedResponse); + RunState.LastUpdatedAtUtc = AssistedResponse.RespondedAtUtc; + RunState = UHyperTwistTrainingLibrary::AdvanceMethodDrillCase(RunState); + RunState = UHyperTwistTrainingLibrary::StartNextMethodDrillCase(RunState); + if (!RunState.IsStructurallyValid()) + { + return FHyperTwistTrainingMethodDrillDiagnosticPacket(); + } + + FHyperTwistTrainingMethodDrillResponseRecord RepeatResponseA; + RepeatResponseA.ResponseId = TEXT("method_drill_diag_response_02"); + RepeatResponseA.DrillRunId = RunState.RunId; + RepeatResponseA.CaseId = RunState.CurrentCase.CaseId; + RepeatResponseA.PresentedAtUtc = TEXT("2026-04-29T09:00:20Z"); + RepeatResponseA.RespondedAtUtc = TEXT("2026-04-29T09:00:31Z"); + RepeatResponseA.DrillMode = RunState.DrillDefinition.DrillMode; + RepeatResponseA.VisibilityPolicy = RunState.DrillDefinition.VisibilityPolicy; + RepeatResponseA.bAnswerWasRevealed = false; + RepeatResponseA.bMarkedCorrect = false; + RepeatResponseA.bWasRepeat = false; + RepeatResponseA.ElapsedMs = 2400; + RepeatResponseA.ResponseText = TEXT("missed block anchor on first pass"); + RunState.Responses.Add(RepeatResponseA); + RunState.LastUpdatedAtUtc = RepeatResponseA.RespondedAtUtc; + RunState = UHyperTwistTrainingLibrary::RepeatMethodDrillCase(RunState); + + FHyperTwistTrainingMethodDrillResponseRecord RepeatResponseB; + RepeatResponseB.ResponseId = TEXT("method_drill_diag_response_03"); + RepeatResponseB.DrillRunId = RunState.RunId; + RepeatResponseB.CaseId = RunState.CurrentCase.CaseId; + RepeatResponseB.PresentedAtUtc = TEXT("2026-04-29T09:00:35Z"); + RepeatResponseB.RespondedAtUtc = TEXT("2026-04-29T09:00:42Z"); + RepeatResponseB.DrillMode = RunState.DrillDefinition.DrillMode; + RepeatResponseB.VisibilityPolicy = RunState.DrillDefinition.VisibilityPolicy; + RepeatResponseB.bAnswerWasRevealed = false; + RepeatResponseB.bMarkedCorrect = true; + RepeatResponseB.bWasRepeat = true; + RepeatResponseB.ElapsedMs = 1900; + RepeatResponseB.ResponseText = TEXT("corrected block anchor after repeat"); + RunState.Responses.Add(RepeatResponseB); + RunState.LastUpdatedAtUtc = RepeatResponseB.RespondedAtUtc; + + return UHyperTwistTrainingLibrary::DeriveMethodDrillDiagnosticPacket(RunState); +} + +FHyperTwistTrainingSessionTemplate UHyperTwistContractLibrary::MakeSampleMethodDrillFollowUpTemplate() +{ + return UHyperTwistTrainingRepositoryLibrary::BuildMethodDrillFollowUpTemplate( + MakeSampleMethodDrillDiagnosticPacket(), + TEXT("method_drill_follow_up_template_01") + ); +} + FHyperTwistTrainingRunStepResult UHyperTwistContractLibrary::MakeSampleTrainingRunStepResult() { const FHyperTwistTrainingRunState RunState = MakeSampleTrainingRunState(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp index 117b1b6..98eff73 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingLibrary.cpp @@ -637,6 +637,297 @@ namespace HyperTwistTrainingLibraryInternal Summary.LastUpdatedAtUtc = RunState.LastUpdatedAtUtc; return Summary; } + + struct FMethodDrillCaseMetrics + { + int32 AttemptCount = 0; + int32 CorrectResponseCount = 0; + int32 IncorrectResponseCount = 0; + int32 RevealCount = 0; + int32 RepeatCount = 0; + int64 ElapsedAccumulator = 0; + int32 ElapsedSampleCount = 0; + int32 BestElapsedMs = 0; + int32 WorstElapsedMs = 0; + }; + + TArray BuildKnownDrillCaseIds(const FHyperTwistTrainingMethodDrillRunState& RunState) + { + TArray KnownCaseIds = BuildDrillCaseIds(RunState.DrillDefinition); + for (const FString& CaseId : RunState.PendingCaseIds) + { + if (!CaseId.IsEmpty()) + { + KnownCaseIds.AddUnique(CaseId); + } + } + for (const FString& CaseId : RunState.CompletedCaseIds) + { + if (!CaseId.IsEmpty()) + { + KnownCaseIds.AddUnique(CaseId); + } + } + if (!RunState.CurrentCase.CaseId.IsEmpty()) + { + KnownCaseIds.AddUnique(RunState.CurrentCase.CaseId); + } + for (const FHyperTwistTrainingMethodDrillResponseRecord& Response : RunState.Responses) + { + if (!Response.CaseId.IsEmpty()) + { + KnownCaseIds.AddUnique(Response.CaseId); + } + } + + return KnownCaseIds; + } + + FMethodDrillCaseMetrics CollectMethodDrillCaseMetrics( + const FHyperTwistTrainingMethodDrillRunState& RunState, + const FString& CaseId + ) + { + FMethodDrillCaseMetrics Metrics; + for (const FHyperTwistTrainingMethodDrillResponseRecord& Response : RunState.Responses) + { + if (Response.CaseId != CaseId) + { + continue; + } + + Metrics.AttemptCount += 1; + Metrics.CorrectResponseCount += Response.bMarkedCorrect ? 1 : 0; + Metrics.IncorrectResponseCount += Response.bMarkedCorrect ? 0 : 1; + Metrics.RevealCount += Response.bAnswerWasRevealed ? 1 : 0; + Metrics.RepeatCount += Response.bWasRepeat ? 1 : 0; + + if (Response.ElapsedMs > 0) + { + Metrics.ElapsedAccumulator += Response.ElapsedMs; + Metrics.ElapsedSampleCount += 1; + Metrics.BestElapsedMs = Metrics.BestElapsedMs == 0 + ? Response.ElapsedMs + : FMath::Min(Metrics.BestElapsedMs, Response.ElapsedMs); + Metrics.WorstElapsedMs = FMath::Max(Metrics.WorstElapsedMs, Response.ElapsedMs); + } + } + + return Metrics; + } + + EHyperTwistTrainingMethodDrillDiagnosticState ResolveMethodDrillCaseDiagnosticState( + const FMethodDrillCaseMetrics& Metrics, + bool bCompleted, + bool bStillPending + ) + { + if (bStillPending) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::Deferred; + } + if (Metrics.RepeatCount > 0 || Metrics.IncorrectResponseCount > 0) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::RepeatBurden; + } + if (Metrics.RevealCount > 0) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::AssistedCompletion; + } + if (bCompleted) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::CleanCompletion; + } + + return EHyperTwistTrainingMethodDrillDiagnosticState::Unknown; + } + + EHyperTwistTrainingReviewGrade ResolveMethodDrillCaseReviewGrade( + const FMethodDrillCaseMetrics& Metrics, + bool bCompleted, + bool bStillPending, + bool bWasCarryoverCase + ) + { + if (bStillPending) + { + return EHyperTwistTrainingReviewGrade::Again; + } + if (Metrics.RepeatCount > 1 || Metrics.IncorrectResponseCount > 0) + { + return EHyperTwistTrainingReviewGrade::Again; + } + if (Metrics.RepeatCount == 1 || Metrics.RevealCount > 0) + { + return EHyperTwistTrainingReviewGrade::Hard; + } + if (bWasCarryoverCase) + { + return EHyperTwistTrainingReviewGrade::Good; + } + if (bCompleted && Metrics.AttemptCount > 0 && Metrics.CorrectResponseCount == Metrics.AttemptCount) + { + return EHyperTwistTrainingReviewGrade::Easy; + } + if (bCompleted) + { + return EHyperTwistTrainingReviewGrade::Good; + } + + return EHyperTwistTrainingReviewGrade::Hard; + } + + FString BuildMethodDrillCaseBoundaryReason( + const FMethodDrillCaseMetrics& Metrics, + bool bCompleted, + bool bStillPending, + bool bWasCarryoverCase + ) + { + if (bStillPending) + { + return TEXT("case remains pending or active and should stay in follow-up focus"); + } + if (Metrics.RepeatCount > 1 || Metrics.IncorrectResponseCount > 0) + { + return TEXT("case required repeated or incorrect responses before stabilization"); + } + if (Metrics.RepeatCount == 1) + { + return TEXT("case required an immediate repeat before closure"); + } + if (Metrics.RevealCount > 0) + { + return TEXT("case depended on answer reveal assistance"); + } + if (bWasCarryoverCase) + { + return TEXT("case closed cleanly after resuming carried-over drill work"); + } + if (bCompleted) + { + return TEXT("case closed cleanly without reveal or repeat pressure"); + } + + return TEXT("case has no actionable diagnostic boundary"); + } + + float BuildMethodDrillCasePriorityScore( + const FMethodDrillCaseMetrics& Metrics, + bool bStillPending, + bool bWasCarryoverCase + ) + { + float Score = 0.0f; + if (bStillPending) + { + Score += 100.0f; + } + + Score += static_cast(Metrics.RepeatCount) * 45.0f; + Score += static_cast(Metrics.IncorrectResponseCount) * 30.0f; + if (Metrics.RevealCount > 0) + { + Score += 20.0f; + } + if (bWasCarryoverCase) + { + Score += 10.0f; + } + + return Score; + } + + FString BuildMethodDrillRunBoundaryReason(const FHyperTwistTrainingMethodDrillDiagnosticPacket& Packet) + { + if (Packet.bBoundaryTriggeredByPendingCases) + { + return TEXT("deferred drill work remains open and should be revisited first"); + } + if (Packet.bBoundaryTriggeredByRepeatBurden) + { + return TEXT("repeat burden or incorrect responses still dominate the follow-up set"); + } + if (Packet.bBoundaryTriggeredByAssistance) + { + return TEXT("answer reveal assistance was needed on completed drill cases"); + } + if (Packet.bBoundaryTriggeredByResumeCarryover) + { + return TEXT("resumed carryover work closed cleanly but should remain reviewable"); + } + if (Packet.CompletedCaseCount > 0 && Packet.CompletedCaseCount == Packet.TotalCaseCount) + { + return TEXT("drill completed cleanly without reveal or repeat pressure"); + } + + return TEXT("drill diagnostic packet is neutral"); + } + + EHyperTwistTrainingReviewGrade ResolveMethodDrillRunReviewGrade( + const FHyperTwistTrainingMethodDrillDiagnosticPacket& Packet + ) + { + if (Packet.bBoundaryTriggeredByPendingCases) + { + return EHyperTwistTrainingReviewGrade::Again; + } + + const bool bHasAgainCase = Packet.CasePackets.ContainsByPredicate( + [](const FHyperTwistTrainingMethodDrillCaseComparisonPacket& CasePacket) + { + return CasePacket.RecommendedReviewGrade == EHyperTwistTrainingReviewGrade::Again; + } + ); + if (bHasAgainCase) + { + return EHyperTwistTrainingReviewGrade::Again; + } + if (Packet.bBoundaryTriggeredByRepeatBurden || Packet.bBoundaryTriggeredByAssistance) + { + return EHyperTwistTrainingReviewGrade::Hard; + } + if (Packet.bBoundaryTriggeredByResumeCarryover) + { + return EHyperTwistTrainingReviewGrade::Good; + } + if (Packet.CompletedCaseCount > 0 + && Packet.CompletedCaseCount == Packet.TotalCaseCount + && Packet.ResponseCount >= Packet.CompletedCaseCount) + { + return EHyperTwistTrainingReviewGrade::Easy; + } + if (Packet.CompletedCaseCount == Packet.TotalCaseCount) + { + return EHyperTwistTrainingReviewGrade::Good; + } + + return EHyperTwistTrainingReviewGrade::Hard; + } + + EHyperTwistTrainingMethodDrillDiagnosticState ResolveMethodDrillRunDiagnosticState( + const FHyperTwistTrainingMethodDrillDiagnosticPacket& Packet + ) + { + if (Packet.bBoundaryTriggeredByPendingCases) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::Deferred; + } + if (Packet.bBoundaryTriggeredByRepeatBurden) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::RepeatBurden; + } + if (Packet.bBoundaryTriggeredByAssistance) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::AssistedCompletion; + } + if (Packet.CompletedCaseCount > 0 && Packet.CompletedCaseCount == Packet.TotalCaseCount) + { + return EHyperTwistTrainingMethodDrillDiagnosticState::CleanCompletion; + } + + return EHyperTwistTrainingMethodDrillDiagnosticState::Unknown; + } } FHyperTwistContentPack UHyperTwistTrainingLibrary::MakeSampleClassicContentPack() @@ -1936,6 +2227,160 @@ FHyperTwistTrainingMethodDrillSummary UHyperTwistTrainingLibrary::DeriveMethodDr return HyperTwistTrainingLibraryInternal::BuildDrillSummaryInternal(RunState); } +FHyperTwistTrainingMethodDrillDiagnosticPacket UHyperTwistTrainingLibrary::DeriveMethodDrillDiagnosticPacket( + const FHyperTwistTrainingMethodDrillRunState& RunState +) +{ + FHyperTwistTrainingMethodDrillDiagnosticPacket Packet; + if (!RunState.IsStructurallyValid()) + { + return Packet; + } + + Packet.RunId = RunState.RunId; + Packet.UserId = RunState.UserId; + Packet.DrillId = RunState.DrillDefinition.DrillId; + Packet.SourceDeckId = RunState.DrillDefinition.SourceDeckId; + Packet.MethodSegmentId = RunState.DrillDefinition.MethodDescriptor.SegmentId; + Packet.StageId = RunState.DrillDefinition.StageId; + Packet.StageLabel = RunState.DrillDefinition.StageLabel; + Packet.SourceBatchId = RunState.SourceBatchId; + Packet.ReferenceUtc = HyperTwistTrainingLibraryInternal::ResolveDrillTimestamp(RunState.LastUpdatedAtUtc); + Packet.DrillMode = RunState.DrillDefinition.DrillMode; + Packet.VisibilityPolicy = RunState.DrillDefinition.VisibilityPolicy; + Packet.LifecycleState = RunState.LifecycleState; + Packet.ResponseCount = RunState.Responses.Num(); + Packet.bWasResumedRun = !RunState.SourceBatchId.IsEmpty(); + + const TArray KnownCaseIds = HyperTwistTrainingLibraryInternal::BuildKnownDrillCaseIds(RunState); + TSet CompletedCaseSet(RunState.CompletedCaseIds); + TSet UnresolvedCaseSet; + for (const FString& CaseId : RunState.PendingCaseIds) + { + if (!CaseId.IsEmpty()) + { + UnresolvedCaseSet.Add(CaseId); + } + } + if (!RunState.CurrentCase.CaseId.IsEmpty()) + { + UnresolvedCaseSet.Add(RunState.CurrentCase.CaseId); + } + for (const FString& CompletedCaseId : RunState.CompletedCaseIds) + { + UnresolvedCaseSet.Remove(CompletedCaseId); + } + + Packet.TotalCaseCount = KnownCaseIds.Num(); + Packet.CompletedCaseCount = CompletedCaseSet.Num(); + Packet.PendingCaseCount = UnresolvedCaseSet.Num(); + + float MaxCasePriority = 0.0f; + for (const FString& CaseId : KnownCaseIds) + { + const HyperTwistTrainingLibraryInternal::FMethodDrillCaseMetrics Metrics = + HyperTwistTrainingLibraryInternal::CollectMethodDrillCaseMetrics(RunState, CaseId); + const bool bCompleted = CompletedCaseSet.Contains(CaseId); + const bool bStillPending = UnresolvedCaseSet.Contains(CaseId); + const bool bWasCarryoverCase = Packet.bWasResumedRun; + + FHyperTwistTrainingMethodDrillCaseComparisonPacket CasePacket; + CasePacket.CaseId = CaseId; + CasePacket.AttemptCount = Metrics.AttemptCount; + CasePacket.CorrectResponseCount = Metrics.CorrectResponseCount; + CasePacket.IncorrectResponseCount = Metrics.IncorrectResponseCount; + CasePacket.RevealCount = Metrics.RevealCount; + CasePacket.RepeatCount = Metrics.RepeatCount; + CasePacket.AverageElapsedMs = Metrics.ElapsedSampleCount > 0 + ? static_cast(Metrics.ElapsedAccumulator / Metrics.ElapsedSampleCount) + : 0; + CasePacket.BestElapsedMs = Metrics.BestElapsedMs; + CasePacket.WorstElapsedMs = Metrics.WorstElapsedMs; + CasePacket.bCompleted = bCompleted; + CasePacket.bStillPending = bStillPending; + CasePacket.bWasCarryoverCase = bWasCarryoverCase; + CasePacket.DiagnosticState = HyperTwistTrainingLibraryInternal::ResolveMethodDrillCaseDiagnosticState( + Metrics, + bCompleted, + bStillPending + ); + CasePacket.RecommendedReviewGrade = HyperTwistTrainingLibraryInternal::ResolveMethodDrillCaseReviewGrade( + Metrics, + bCompleted, + bStillPending, + bWasCarryoverCase + ); + CasePacket.bBoundaryTriggeredByReveal = Metrics.RevealCount > 0; + CasePacket.bBoundaryTriggeredByRepeat = Metrics.RepeatCount > 0 || Metrics.IncorrectResponseCount > 0; + CasePacket.bBoundaryTriggeredByDeferredState = bStillPending; + CasePacket.bBoundaryTriggeredByCarryover = bWasCarryoverCase; + CasePacket.BoundaryReason = HyperTwistTrainingLibraryInternal::BuildMethodDrillCaseBoundaryReason( + Metrics, + bCompleted, + bStillPending, + bWasCarryoverCase + ); + CasePacket.PriorityScore = HyperTwistTrainingLibraryInternal::BuildMethodDrillCasePriorityScore( + Metrics, + bStillPending, + bWasCarryoverCase + ); + + Packet.CorrectResponseCount += Metrics.CorrectResponseCount; + Packet.IncorrectResponseCount += Metrics.IncorrectResponseCount; + Packet.RevealedCaseCount += Metrics.RevealCount > 0 ? 1 : 0; + Packet.AssistedCompletionCount += CasePacket.bBoundaryTriggeredByReveal && bCompleted ? 1 : 0; + Packet.RepeatBurdenCaseCount += CasePacket.bBoundaryTriggeredByRepeat ? 1 : 0; + Packet.DeferredCaseCount += + CasePacket.DiagnosticState == EHyperTwistTrainingMethodDrillDiagnosticState::Deferred ? 1 : 0; + Packet.ResolvedCarryoverCaseCount += bWasCarryoverCase && bCompleted ? 1 : 0; + MaxCasePriority = FMath::Max(MaxCasePriority, CasePacket.PriorityScore); + Packet.CasePackets.Add(CasePacket); + } + + Packet.CasePackets.Sort([]( + const FHyperTwistTrainingMethodDrillCaseComparisonPacket& Left, + const FHyperTwistTrainingMethodDrillCaseComparisonPacket& Right + ) + { + if (!FMath::IsNearlyEqual(Left.PriorityScore, Right.PriorityScore)) + { + return Left.PriorityScore > Right.PriorityScore; + } + return Left.CaseId < Right.CaseId; + }); + + for (const FHyperTwistTrainingMethodDrillCaseComparisonPacket& CasePacket : Packet.CasePackets) + { + if (CasePacket.PriorityScore > 0.0f) + { + Packet.FocusCaseIds.Add(CasePacket.CaseId); + } + } + + Packet.CompletionFraction = Packet.TotalCaseCount > 0 + ? static_cast(Packet.CompletedCaseCount) / static_cast(Packet.TotalCaseCount) + : 0.0f; + const int32 AssistanceCaseCount = Packet.AssistedCompletionCount + Packet.RepeatBurdenCaseCount; + Packet.AssistanceFraction = Packet.TotalCaseCount > 0 + ? static_cast(AssistanceCaseCount) / static_cast(Packet.TotalCaseCount) + : 0.0f; + Packet.bBoundaryTriggeredByPendingCases = Packet.DeferredCaseCount > 0; + Packet.bBoundaryTriggeredByRepeatBurden = Packet.RepeatBurdenCaseCount > 0; + Packet.bBoundaryTriggeredByAssistance = Packet.AssistedCompletionCount > 0; + Packet.bBoundaryTriggeredByResumeCarryover = Packet.bWasResumedRun; + Packet.bNeedsFollowUpTemplate = Packet.FocusCaseIds.Num() > 0; + Packet.bNeedsReviewPlanBridge = Packet.FocusCaseIds.Num() > 0 && !Packet.bBoundaryTriggeredByPendingCases; + Packet.PriorityScore = MaxCasePriority + + static_cast(Packet.DeferredCaseCount * 10) + + static_cast(Packet.RepeatBurdenCaseCount * 5) + + static_cast(Packet.AssistedCompletionCount * 2); + Packet.DiagnosticState = HyperTwistTrainingLibraryInternal::ResolveMethodDrillRunDiagnosticState(Packet); + Packet.RecommendedReviewGrade = HyperTwistTrainingLibraryInternal::ResolveMethodDrillRunReviewGrade(Packet); + Packet.BoundaryReason = HyperTwistTrainingLibraryInternal::BuildMethodDrillRunBoundaryReason(Packet); + return Packet; +} + FHyperTwistTrainingMethodDrillBatch UHyperTwistTrainingLibrary::BuildMethodDrillBatch( const FHyperTwistTrainingMethodDrillRunState& RunState, const FString& BatchId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index d8257f9..a54e778 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -3158,6 +3158,82 @@ FHyperTwistTrainingMethodDrillDefinition UHyperTwistTrainingRepositoryLibrary::B return Result; } +FHyperTwistTrainingSessionTemplate UHyperTwistTrainingRepositoryLibrary::BuildMethodDrillFollowUpTemplate( + const FHyperTwistTrainingMethodDrillDiagnosticPacket& DiagnosticPacket, + const FString& TemplateId +) +{ + FHyperTwistTrainingSessionTemplate SessionTemplate; + if (!DiagnosticPacket.IsStructurallyValid() + || !DiagnosticPacket.bNeedsFollowUpTemplate + || DiagnosticPacket.SourceDeckId.IsEmpty()) + { + return SessionTemplate; + } + + SessionTemplate.UserId = DiagnosticPacket.UserId; + SessionTemplate.TemplateId = TemplateId.IsEmpty() + ? FString::Printf(TEXT("method_drill_follow_up_%s"), *DiagnosticPacket.RunId) + : TemplateId; + SessionTemplate.Title = !DiagnosticPacket.StageLabel.IsEmpty() + ? FString::Printf(TEXT("%s Drill Follow-Up"), *DiagnosticPacket.StageLabel) + : TEXT("Method Drill Follow-Up"); + SessionTemplate.TemplateKind = + DiagnosticPacket.bBoundaryTriggeredByPendingCases || DiagnosticPacket.bBoundaryTriggeredByRepeatBurden + ? EHyperTwistTrainingSessionTemplateKind::Recovery + : EHyperTwistTrainingSessionTemplateKind::ReviewPlan; + SessionTemplate.FocusDeckId = DiagnosticPacket.SourceDeckId; + SessionTemplate.MethodSegmentId = DiagnosticPacket.MethodSegmentId; + + switch (DiagnosticPacket.DrillMode) + { + case EHyperTwistTrainingDrillModeKind::Recognition: + case EHyperTwistTrainingDrillModeKind::Reconstruction: + SessionTemplate.SuggestedMode = EHyperTwistTrainingDeliveryMode::RecognitionAssisted; + break; + case EHyperTwistTrainingDrillModeKind::Execution: + SessionTemplate.SuggestedMode = EHyperTwistTrainingDeliveryMode::VirtualCube; + break; + case EHyperTwistTrainingDrillModeKind::Recall: + default: + SessionTemplate.SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + break; + } + + SessionTemplate.SuggestedSelectionPolicy = + DiagnosticPacket.bBoundaryTriggeredByPendingCases + ? EHyperTwistTrainingSelectionPolicy::Scripted + : EHyperTwistTrainingSelectionPolicy::Weighted; + SessionTemplate.FocusCaseIds = + HyperTwistTrainingRepositoryLibraryInternal::DeduplicateTemplateCaseIds(DiagnosticPacket.FocusCaseIds); + if (SessionTemplate.FocusCaseIds.Num() == 0) + { + return FHyperTwistTrainingSessionTemplate(); + } + + SessionTemplate.RecommendedCaseCount = FMath::Max( + 1, + FMath::Min( + SessionTemplate.FocusCaseIds.Num(), + DiagnosticPacket.RecommendedReviewGrade == EHyperTwistTrainingReviewGrade::Again ? 6 : 4 + ) + ); + SessionTemplate.GuidanceLabel = !DiagnosticPacket.BoundaryReason.IsEmpty() + ? DiagnosticPacket.BoundaryReason + : TEXT("method-drill follow-up"); + SessionTemplate.bPreferReviewRun = + DiagnosticPacket.bNeedsReviewPlanBridge + || DiagnosticPacket.RecommendedReviewGrade != EHyperTwistTrainingReviewGrade::Easy; + SessionTemplate.bCoachReview = false; + SessionTemplate.bPreferShortReviewSet = SessionTemplate.RecommendedCaseCount <= 4; + SessionTemplate.bUserEditable = false; + SessionTemplate.bUserCustomized = false; + SessionTemplate.bUserCreated = false; + SessionTemplate.LastUpdatedAtUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(DiagnosticPacket.ReferenceUtc); + return SessionTemplate.IsStructurallyValid() ? SessionTemplate : FHyperTwistTrainingSessionTemplate(); +} + FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertRunRecord( const FHyperTwistTrainingRepositoryState& RepositoryState, const FHyperTwistTrainingRunRecord& RunRecord diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index 27e08fc..096d436 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -92,6 +92,28 @@ FHyperTwistTrainingMethodDrillSummary UHyperTwistTrainingRuntimeLibrary::GetActi return FHyperTwistTrainingMethodDrillSummary(); } +FHyperTwistTrainingMethodDrillDiagnosticPacket +UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingMethodDrillDiagnosticPacket(UObject* WorldContextObject) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveMethodDrillDiagnosticPacket(); + } + + return FHyperTwistTrainingMethodDrillDiagnosticPacket(); +} + +FHyperTwistTrainingSessionTemplate +UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingMethodDrillFollowUpTemplate(UObject* WorldContextObject) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveMethodDrillFollowUpTemplate(); + } + + return FHyperTwistTrainingSessionTemplate(); +} + FHyperTwistTrainingRepositoryState UHyperTwistTrainingRuntimeLibrary::GetTrainingRepositoryState(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 241bf5a..e8bd5d7 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -425,6 +425,16 @@ FHyperTwistTrainingMethodDrillSummary UHyperTwistTrainingSubsystem::GetActiveMet return ActiveMethodDrillSummary; } +FHyperTwistTrainingMethodDrillDiagnosticPacket UHyperTwistTrainingSubsystem::GetActiveMethodDrillDiagnosticPacket() const +{ + return ActiveMethodDrillDiagnosticPacket; +} + +FHyperTwistTrainingSessionTemplate UHyperTwistTrainingSubsystem::GetActiveMethodDrillFollowUpTemplate() const +{ + return ActiveMethodDrillFollowUpTemplate; +} + FHyperTwistTrainingRepositoryState UHyperTwistTrainingSubsystem::GetTrainingRepositoryState() const { return TrainingRepositoryState; @@ -2364,6 +2374,8 @@ void UHyperTwistTrainingSubsystem::ClearActiveMethodDrillRun() { ActiveMethodDrillRunState = FHyperTwistTrainingMethodDrillRunState(); ActiveMethodDrillSummary = FHyperTwistTrainingMethodDrillSummary(); + ActiveMethodDrillDiagnosticPacket = FHyperTwistTrainingMethodDrillDiagnosticPacket(); + ActiveMethodDrillFollowUpTemplate = FHyperTwistTrainingSessionTemplate(); bHasActiveMethodDrillRun = false; } @@ -2386,12 +2398,23 @@ void UHyperTwistTrainingSubsystem::RefreshMethodDrillSummary() if (!ActiveMethodDrillRunState.IsStructurallyValid()) { ActiveMethodDrillSummary = FHyperTwistTrainingMethodDrillSummary(); + ActiveMethodDrillDiagnosticPacket = FHyperTwistTrainingMethodDrillDiagnosticPacket(); + ActiveMethodDrillFollowUpTemplate = FHyperTwistTrainingSessionTemplate(); return; } ActiveMethodDrillSummary = UHyperTwistTrainingLibrary::DeriveMethodDrillSummary( ActiveMethodDrillRunState ); + ActiveMethodDrillDiagnosticPacket = UHyperTwistTrainingLibrary::DeriveMethodDrillDiagnosticPacket( + ActiveMethodDrillRunState + ); + ActiveMethodDrillFollowUpTemplate = ActiveMethodDrillDiagnosticPacket.IsStructurallyValid() + ? UHyperTwistTrainingRepositoryLibrary::BuildMethodDrillFollowUpTemplate( + ActiveMethodDrillDiagnosticPacket, + FString() + ) + : FHyperTwistTrainingSessionTemplate(); } void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h index ef44154..4441012 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h @@ -92,6 +92,12 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") static FHyperTwistTrainingDrillCaseModeCompatibility MakeSampleMethodDrillCaseModeCompatibility(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillDiagnosticPacket MakeSampleMethodDrillDiagnosticPacket(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingSessionTemplate MakeSampleMethodDrillFollowUpTemplate(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") static FHyperTwistTrainingRunStepResult MakeSampleTrainingRunStepResult(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h index acf4563..c89ac8d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingLibrary.h @@ -201,6 +201,11 @@ public: const FHyperTwistTrainingMethodDrillRunState& RunState ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + static FHyperTwistTrainingMethodDrillDiagnosticPacket DeriveMethodDrillDiagnosticPacket( + const FHyperTwistTrainingMethodDrillRunState& RunState + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") static FHyperTwistTrainingMethodDrillBatch BuildMethodDrillBatch( const FHyperTwistTrainingMethodDrillRunState& RunState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h index 89d7e53..c8d9e21 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h @@ -124,6 +124,12 @@ public: int32 MaxFavoriteCases ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Drill") + static FHyperTwistTrainingSessionTemplate BuildMethodDrillFollowUpTemplate( + const FHyperTwistTrainingMethodDrillDiagnosticPacket& DiagnosticPacket, + const FString& TemplateId + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository") static FHyperTwistTrainingRepositoryState UpsertRunRecord( const FHyperTwistTrainingRepositoryState& RepositoryState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index d36e4e6..31059fd 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -37,6 +37,16 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingMethodDrillSummary GetActiveMethodDrillSummary(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingMethodDrillDiagnosticPacket GetActiveTrainingMethodDrillDiagnosticPacket( + UObject* WorldContextObject + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingSessionTemplate GetActiveTrainingMethodDrillFollowUpTemplate( + UObject* WorldContextObject + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRepositoryState GetTrainingRepositoryState(UObject* WorldContextObject); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 11a2201..fbb06fd 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -32,6 +32,12 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") FHyperTwistTrainingMethodDrillSummary GetActiveMethodDrillSummary() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingMethodDrillDiagnosticPacket GetActiveMethodDrillDiagnosticPacket() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Drill") + FHyperTwistTrainingSessionTemplate GetActiveMethodDrillFollowUpTemplate() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") FHyperTwistTrainingRepositoryState GetTrainingRepositoryState() const; @@ -467,6 +473,12 @@ private: UPROPERTY() FHyperTwistTrainingMethodDrillSummary ActiveMethodDrillSummary; + UPROPERTY() + FHyperTwistTrainingMethodDrillDiagnosticPacket ActiveMethodDrillDiagnosticPacket; + + UPROPERTY() + FHyperTwistTrainingSessionTemplate ActiveMethodDrillFollowUpTemplate; + UPROPERTY() FHyperTwistTrainingRepositoryState TrainingRepositoryState; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 70129b3..e9f4aeb 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -155,6 +155,16 @@ enum class EHyperTwistTrainingDrillLifecycleState : uint8 Aborted UMETA(DisplayName = "Aborted") }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingMethodDrillDiagnosticState : uint8 +{ + Unknown UMETA(DisplayName = "Unknown"), + CleanCompletion UMETA(DisplayName = "Clean Completion"), + AssistedCompletion UMETA(DisplayName = "Assisted Completion"), + RepeatBurden UMETA(DisplayName = "Repeat Burden"), + Deferred UMETA(DisplayName = "Deferred") +}; + UENUM(BlueprintType) enum class EHyperTwistTrainingDrillEntrySource : uint8 { @@ -2460,6 +2470,205 @@ struct FHyperTwistTrainingDrillCaseModeCompatibility } }; +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillCaseComparisonPacket +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CorrectResponseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 IncorrectResponseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RevealCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RepeatCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BestElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 WorstElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCompleted = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bStillPending = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWasCarryoverCase = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingMethodDrillDiagnosticState DiagnosticState = + EHyperTwistTrainingMethodDrillDiagnosticState::Unknown; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingReviewGrade RecommendedReviewGrade = EHyperTwistTrainingReviewGrade::Good; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByReveal = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByRepeat = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByDeferredState = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByCarryover = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BoundaryReason; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PriorityScore = 0.0f; + + bool IsStructurallyValid() const + { + return !CaseId.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingMethodDrillDiagnosticPacket +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RunId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DrillId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StageId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StageLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceBatchId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillModeKind DrillMode = EHyperTwistTrainingDrillModeKind::Recognition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillVisibilityPolicy VisibilityPolicy = + EHyperTwistTrainingDrillVisibilityPolicy::FullInformation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDrillLifecycleState LifecycleState = EHyperTwistTrainingDrillLifecycleState::Ready; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PendingCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ResponseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CorrectResponseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 IncorrectResponseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RevealedCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AssistedCompletionCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RepeatBurdenCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DeferredCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ResolvedCarryoverCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float CompletionFraction = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float AssistanceFraction = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWasResumedRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsFollowUpTemplate = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsReviewPlanBridge = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingMethodDrillDiagnosticState DiagnosticState = + EHyperTwistTrainingMethodDrillDiagnosticState::Unknown; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingReviewGrade RecommendedReviewGrade = EHyperTwistTrainingReviewGrade::Good; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByPendingCases = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByRepeatBurden = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByAssistance = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bBoundaryTriggeredByResumeCarryover = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString BoundaryReason; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PriorityScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray CasePackets; + + bool IsStructurallyValid() const + { + return !RunId.IsEmpty() && !UserId.IsEmpty() && !DrillId.IsEmpty(); + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingLearnerPreset {