diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index ed18c87..0eeac86 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -1,5 +1,6 @@ #include "HyperTwistTraining/HyperTwistCoachDashboardWidget.h" +#include "HyperTwistTraining/HyperTwistTrainingCoachLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingSubsystem.h" @@ -20013,33 +20014,11 @@ FString UHyperTwistCoachDashboardWidget::BuildDefaultDeferredUntilUtc() const FString UHyperTwistCoachDashboardWidget::FindFirstDeferredQueueEntryId() const { - const FHyperTwistTrainingCoachSessionQueueStateEntry* BestDeferredEntry = nullptr; - float BestDeferredScore = TNumericLimits::Lowest(); - for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : CachedCoachSessionQueueState.Entries) - { - if (Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred) - { - continue; - } - - const float EntryScore = HyperTwistCoachDashboardWidgetInternal::ScoreDeferredEntryForPromotion( - Entry, - CachedCoachPanelState - ); - const bool bShouldReplaceBest = BestDeferredEntry == nullptr - || EntryScore > BestDeferredScore - || (FMath::IsNearlyEqual(EntryScore, BestDeferredScore) - && (Entry.SlotIndex < BestDeferredEntry->SlotIndex - || (Entry.SlotIndex == BestDeferredEntry->SlotIndex - && Entry.PriorityScore > BestDeferredEntry->PriorityScore))); - if (bShouldReplaceBest) - { - BestDeferredEntry = &Entry; - BestDeferredScore = EntryScore; - } - } - - return BestDeferredEntry != nullptr ? BestDeferredEntry->EntryId : FString(); + return UHyperTwistTrainingCoachLibrary::DeriveCoachQueueControlSurface( + CachedCoachSessionQueueState, + CachedCoachPanelState + ) + .PromoteTargetEntryId; } bool UHyperTwistCoachDashboardWidget::TryParseAttemptTimeOverride(int32& OutTimeMs) const diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index a3d19932..ea13575 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -398,6 +398,156 @@ namespace HyperTwistTrainingCoachLibraryInternal return PanelState.ReviewPolicyDetailLine; } + const FHyperTwistTrainingCoachSessionQueueStateEntry* FindQueueEntryById( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId + ) + { + if (EntryId.IsEmpty()) + { + return nullptr; + } + + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : QueueState.Entries) + { + if (Entry.EntryId == EntryId) + { + return &Entry; + } + } + + return nullptr; + } + + float ScoreDeferredEntryForPromotion( + const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry, + const FHyperTwistTrainingCoachPanelState& CoachPanelState + ) + { + float Score = FMath::Clamp(Entry.PriorityScore * 0.02f, 0.0f, 2.0f); + if (!CoachPanelState.PreferredGuidanceSourceLabel.IsEmpty() + && Entry.SourceLabel == CoachPanelState.PreferredGuidanceSourceLabel) + { + Score += 2.0f; + } + + switch (CoachPanelState.PreferredGuidanceLane) + { + case EHyperTwistTrainingCoachGuidanceLane::FollowUp: + if (Entry.bRequiresFollowUp || Entry.SourceLabel == TEXT("follow-up-brief")) + { + Score += 8.0f; + } + if (Entry.bRequiresArchiveRetentionTuning || Entry.SourceLabel == TEXT("archive-policy")) + { + Score -= 4.0f; + } + break; + case EHyperTwistTrainingCoachGuidanceLane::Recommended: + if (Entry.SourceLabel == TEXT("primary-brief")) + { + Score += 7.0f; + } + if (Entry.bRequiresArchiveRetentionTuning || Entry.SourceLabel == TEXT("archive-policy")) + { + Score -= 1.5f; + } + break; + default: + break; + } + + if (Entry.bPinned) + { + Score += 0.5f; + } + + return Score; + } + + const FHyperTwistTrainingCoachSessionQueueStateEntry* FindPreferredDeferredQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistTrainingCoachPanelState& CoachPanelState + ) + { + const FHyperTwistTrainingCoachSessionQueueStateEntry* BestDeferredEntry = nullptr; + float BestDeferredScore = TNumericLimits::Lowest(); + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : QueueState.Entries) + { + if (Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred) + { + continue; + } + + const float EntryScore = ScoreDeferredEntryForPromotion(Entry, CoachPanelState); + const bool bShouldReplaceBest = BestDeferredEntry == nullptr + || EntryScore > BestDeferredScore + || (FMath::IsNearlyEqual(EntryScore, BestDeferredScore) + && (Entry.SlotIndex < BestDeferredEntry->SlotIndex + || (Entry.SlotIndex == BestDeferredEntry->SlotIndex + && Entry.PriorityScore > BestDeferredEntry->PriorityScore))); + if (bShouldReplaceBest) + { + BestDeferredEntry = &Entry; + BestDeferredScore = EntryScore; + } + } + + return BestDeferredEntry; + } + + FString DescribeCoachQueueDeferAvailabilityLine( + const FHyperTwistTrainingCoachPanelState& PanelState, + const FHyperTwistTrainingCoachSessionQueueStateEntry* ActiveQueueEntry + ) + { + if (ActiveQueueEntry != nullptr) + { + return !ActiveQueueEntry->Headline.IsEmpty() + ? FString::Printf( + TEXT("Displayed queue entry ready to defer: %s"), + *ActiveQueueEntry->Headline + ) + : (!PanelState.QueueStatusLine.IsEmpty() + ? PanelState.QueueStatusLine + : TEXT("Displayed queue entry ready to defer")); + } + + if (!PanelState.QueueSuppressionLine.IsEmpty()) + { + return PanelState.QueueSuppressionLine; + } + + return PanelState.bHasQueueWork + ? TEXT("No displayed queue entry ready to defer") + : TEXT("No queue entry ready to defer"); + } + + FString DescribeCoachQueuePromoteAvailabilityLine( + const FHyperTwistTrainingCoachPanelState& PanelState, + const FHyperTwistTrainingCoachSessionQueueStateEntry* DeferredQueueEntry + ) + { + if (DeferredQueueEntry != nullptr) + { + return !DeferredQueueEntry->Headline.IsEmpty() + ? FString::Printf( + TEXT("Deferred queue recovery ready: %s"), + *DeferredQueueEntry->Headline + ) + : TEXT("Deferred queue entry ready to promote"); + } + + if (!PanelState.QueueSuppressionLine.IsEmpty()) + { + return PanelState.QueueSuppressionLine; + } + + return PanelState.bHasDeferredQueueWork + ? TEXT("No deferred queue entry ready to promote") + : TEXT("No deferred queue entry ready to promote"); + } + bool IsQueueRecoveryMemorySource(const FString& SourceLabel) { return SourceLabel.StartsWith(TEXT("coach-memory-queue-")) @@ -5382,3 +5532,42 @@ FHyperTwistTrainingCoachPrimaryActionSurface UHyperTwistTrainingCoachLibrary::De ); return Surface; } + +FHyperTwistTrainingCoachQueueControlSurface +UHyperTwistTrainingCoachLibrary::DeriveCoachQueueControlSurface( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistTrainingCoachPanelState& PanelState +) +{ + FHyperTwistTrainingCoachQueueControlSurface Surface; + Surface.DeferActionLabel = TEXT("defer_queue_entry"); + Surface.PromoteActionLabel = TEXT("promote_deferred_queue_entry"); + + const FHyperTwistTrainingCoachSessionQueueStateEntry* ActiveQueueEntry = + HyperTwistTrainingCoachLibraryInternal::FindQueueEntryById( + QueueState, + PanelState.ActiveQueueEntryId + ); + const FHyperTwistTrainingCoachSessionQueueStateEntry* DeferredQueueEntry = + HyperTwistTrainingCoachLibraryInternal::FindPreferredDeferredQueueEntry( + QueueState, + PanelState + ); + + Surface.DeferTargetEntryId = ActiveQueueEntry != nullptr ? ActiveQueueEntry->EntryId : FString(); + Surface.PromoteTargetEntryId = DeferredQueueEntry != nullptr ? DeferredQueueEntry->EntryId : FString(); + Surface.bCanDefer = ActiveQueueEntry != nullptr; + Surface.bCanPromote = DeferredQueueEntry != nullptr; + Surface.DeferAvailabilityLine = + HyperTwistTrainingCoachLibraryInternal::DescribeCoachQueueDeferAvailabilityLine( + PanelState, + ActiveQueueEntry + ); + Surface.PromoteAvailabilityLine = + HyperTwistTrainingCoachLibraryInternal::DescribeCoachQueuePromoteAvailabilityLine( + PanelState, + DeferredQueueEntry + ); + + return Surface; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index 6d48295..2aab5a1 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -437,6 +437,20 @@ UHyperTwistTrainingPanelWidget::GetDisplayedPrimaryCoachActionSurface() const return Surface; } +FHyperTwistTrainingCoachQueueControlSurface +UHyperTwistTrainingPanelWidget::GetDisplayedCoachQueueControlSurface() const +{ + FHyperTwistTrainingCoachQueueControlSurface Surface = + UHyperTwistTrainingCoachLibrary::DeriveCoachQueueControlSurface( + CachedCoachSessionQueueState, + CachedCoachPanelState + ); + Surface.PromoteActionSourceLabel = Surface.bCanPromote + ? ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Queue) + : FString(); + return Surface; +} + FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachRecommendedRun( const int32 MaxCases, const FString& StartActionSourceLabel @@ -590,13 +604,14 @@ bool UHyperTwistTrainingPanelWidget::DeferDisplayedCoachQueueEntry( const bool bManualReschedule ) { - if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty()) + const FHyperTwistTrainingCoachQueueControlSurface Surface = GetDisplayedCoachQueueControlSurface(); + if (!Surface.bCanDefer || Surface.DeferTargetEntryId.IsEmpty()) { return false; } return DeferCoachQueueEntry( - CachedCoachPanelState.ActiveQueueEntryId, + Surface.DeferTargetEntryId, DeferredUntilUtc, DeferralReasonLabel, bManualReschedule @@ -619,14 +634,15 @@ bool UHyperTwistTrainingPanelWidget::PromoteCoachQueueEntry( bool UHyperTwistTrainingPanelWidget::PromoteDisplayedCoachQueueEntry() { - if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty()) + const FHyperTwistTrainingCoachQueueControlSurface Surface = GetDisplayedCoachQueueControlSurface(); + if (!Surface.bCanPromote || Surface.PromoteTargetEntryId.IsEmpty()) { return false; } return PromoteCoachQueueEntry( - CachedCoachPanelState.ActiveQueueEntryId, - ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Queue) + Surface.PromoteTargetEntryId, + Surface.PromoteActionSourceLabel ); } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp index de6683a..fd54b38 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp @@ -418,6 +418,20 @@ AHyperTwistTrainingSessionActor::GetDisplayedPrimaryCoachActionSurface() const return Surface; } +FHyperTwistTrainingCoachQueueControlSurface +AHyperTwistTrainingSessionActor::GetDisplayedCoachQueueControlSurface() const +{ + FHyperTwistTrainingCoachQueueControlSurface Surface = + UHyperTwistTrainingCoachLibrary::DeriveCoachQueueControlSurface( + CachedCoachSessionQueueState, + CachedCoachPanelState + ); + Surface.PromoteActionSourceLabel = Surface.bCanPromote + ? ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Queue) + : FString(); + return Surface; +} + FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachRecommendedTrainingRun( const int32 MaxCases ) @@ -553,13 +567,14 @@ bool AHyperTwistTrainingSessionActor::DeferDisplayedCoachQueueEntry( const bool bManualReschedule ) { - if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty()) + const FHyperTwistTrainingCoachQueueControlSurface Surface = GetDisplayedCoachQueueControlSurface(); + if (!Surface.bCanDefer || Surface.DeferTargetEntryId.IsEmpty()) { return false; } return DeferCoachQueueEntry( - CachedCoachPanelState.ActiveQueueEntryId, + Surface.DeferTargetEntryId, DeferredUntilUtc, DeferralReasonLabel, bManualReschedule @@ -579,12 +594,13 @@ bool AHyperTwistTrainingSessionActor::PromoteCoachQueueEntry(const FString& Entr bool AHyperTwistTrainingSessionActor::PromoteDisplayedCoachQueueEntry() { - if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty()) + const FHyperTwistTrainingCoachQueueControlSurface Surface = GetDisplayedCoachQueueControlSurface(); + if (!Surface.bCanPromote || Surface.PromoteTargetEntryId.IsEmpty()) { return false; } - return PromoteCoachQueueEntry(CachedCoachPanelState.ActiveQueueEntryId); + return PromoteCoachQueueEntry(Surface.PromoteTargetEntryId); } FHyperTwistTrainingRepositoryRoundTripVerification diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h index 473b7bc..204e307 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -594,4 +594,10 @@ public: static FHyperTwistTrainingCoachPrimaryActionSurface DerivePrimaryCoachActionSurface( const FHyperTwistTrainingCoachPanelState& PanelState ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachQueueControlSurface DeriveCoachQueueControlSurface( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistTrainingCoachPanelState& PanelState + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h index 5b2bb10..38fd954 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h @@ -217,6 +217,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingCoachPrimaryActionSurface GetDisplayedPrimaryCoachActionSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachQueueControlSurface GetDisplayedCoachQueueControlSurface() const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachRecommendedRun( int32 MaxCases, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h index b51af41..0216749 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h @@ -198,6 +198,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingCoachPrimaryActionSurface GetDisplayedPrimaryCoachActionSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachQueueControlSurface GetDisplayedCoachQueueControlSurface() const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartCoachRecommendedTrainingRun(int32 MaxCases); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 75720d2..1ab4ab5 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -7867,3 +7867,44 @@ struct FHyperTwistTrainingCoachPrimaryActionSurface return bCanExecute || !ActionLabel.IsEmpty() || !Headline.IsEmpty() || !AvailabilityLine.IsEmpty(); } }; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachQueueControlSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferAvailabilityLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferTargetEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PromoteActionLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PromoteAvailabilityLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PromoteTargetEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PromoteActionSourceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanDefer = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanPromote = false; + + bool IsStructurallyValid() const + { + return bCanDefer + || bCanPromote + || !DeferAvailabilityLine.IsEmpty() + || !PromoteAvailabilityLine.IsEmpty(); + } +}; diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 8cca080..8bb1271 100644 --- a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md +++ b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md @@ -68,8 +68,8 @@ Current correction call from the source-exposed audit and current execution refr - the imported generated-mode gap is closed; request/config/selection plumbing and the bounded clean-room executor are already landed in first-party code - the latest landed bounded `Phase 4` packet closes the retained-idle comparison/outcome actionability gap at the end of the recognition-assisted coach-to-review continuity lane, and the final retained-surface confirmation pass now leaves that closure lane functionally complete - the retained idle widget surface now keeps retained comparison and decision history inspectable without continuing to advertise live launch or guidance-outcome actions once the coach-to-review loop has already collapsed to truthful closed-loop idle - - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds a thin primary coach CTA descriptor / availability surface over the existing panel-widget and session-actor execution helpers, so gameplay/Blueprint callers can now bind one truthful coach action without replaying queue-vs-follow-up-vs-recommended branch logic from raw booleans - - the current next bounded packet is to add the matching thin secondary queue-control descriptor / availability surface for defer and promote flows on those same two consumer surfaces, without widening into broader UI composition or backend policy work + - the latest landed bounded `Phase 4` packet stays on that same gameplay-facing consumer lane and adds the matching thin secondary queue-control descriptor / availability surface for defer and promote flows on those same two consumer surfaces, while also aligning their displayed promote path with the same deferred-entry recovery ranking already used by the dashboard + - the current next bounded packet is to expose one thin queue-recovery posture / status surface on those same two consumer surfaces so gameplay/Blueprint callers can explain why continue, defer, or promote is preferred without reading raw suppression/status strings or recovery enums directly - the current execution-discipline rule that broad refactor / monolith-splitting work should not interrupt the active bounded roadmap packet unless structure is actually blocking it ## Current execution-reality references diff --git a/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_QUEUE_CONTROL_SURFACE_PACKET_2026-05-07.md b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_QUEUE_CONTROL_SURFACE_PACKET_2026-05-07.md new file mode 100644 index 0000000..89714a2 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_GAMEPLAY_QUEUE_CONTROL_SURFACE_PACKET_2026-05-07.md @@ -0,0 +1,121 @@ +# HyperTwist Phase 4 gameplay queue-control surface packet + +Created on `2026-05-07` + +Status: + +- first-party HyperTwist packet +- bounded Phase `4` gameplay-facing consumer slice + +## Purpose + +This packet closes the next thin gameplay-facing consumer seam above the panel-widget and session-actor coach helpers by exposing one pure secondary queue-control descriptor / availability surface for defer and promote flows. + +The open tasks were: + +- stop forcing gameplay / Blueprint callers to reconstruct defer-vs-promote readiness from raw panel-state and queue-state booleans just to bind the common non-primary queue controls +- expose the actual displayed defer target, the preferred deferred promote target, and promote provenance through a narrow pure surface instead of through caller-side queue inspection +- align `PromoteDisplayedCoachQueueEntry()` on the panel-widget and session-actor with the same deferred-entry recovery ranking already used by the dashboard, instead of assuming the active queue entry is the promote target + +It is not: + +- a new queue-policy packet +- a broader gameplay UI composition pass +- a new default deferral-window flow +- a backend continuity or template-analytics packet + +## Scope + +Bounded lane: + +- add a first-party secondary queue-control surface type in shared training types +- add a coach-library helper that derives defer/promote availability from the retained queue state plus coach panel state +- expose that derived surface on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor` +- keep the derived promote target aligned with the existing dashboard deferred-entry recovery ranking +- route the panel/session displayed defer and displayed promote helpers through that same derived surface + +Out of scope: + +- changing queue recovery policy +- adding dashboard-only selection state to the gameplay consumers +- introducing new queue UI widgets +- widening into broader passive status composition in the same packet + +## Why this was the right next packet + +Before this slice: + +- gameplay-facing panel/session consumers already had displayed queue execution helpers +- those surfaces already had a primary coach CTA surface and execution provenance +- callers could launch the current primary action without dashboard code + +But one thin consumer seam was still open: + +- callers still had to inspect raw queue-state and panel-state booleans to decide whether defer or promote should be shown +- the panel-widget and session-actor still promoted `ActiveQueueEntryId`, even though the dashboard already promotes the best deferred recovery candidate instead + +That meant: + +- primary coach action rendering was thin +- but truthful secondary queue-control rendering and promotion targeting still drifted across consumers + +So the next bounded move was: + +- keep queue policy unchanged +- expose one narrow pure queue-control surface +- and route the displayed promote path through the same deferred-entry recovery target selection already used on the dashboard + +## What landed + +Primary code changes: + +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h` + - added `FHyperTwistTrainingCoachQueueControlSurface` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp` + - added `DeriveCoachQueueControlSurface(...)` + - centralized deferred-entry promotion scoring and preferred deferred-target resolution for shared consumers +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp` + - added `GetDisplayedCoachQueueControlSurface()` + - `DeferDisplayedCoachQueueEntry(...)` and `PromoteDisplayedCoachQueueEntry()` now route through the derived surface instead of directly trusting `ActiveQueueEntryId` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h` +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp` + - added `GetDisplayedCoachQueueControlSurface()` + - the actor-side displayed defer/promote helpers now route through the same derived surface +- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp` + - `FindFirstDeferredQueueEntryId()` now reuses the shared queue-control derivation instead of holding a duplicate deferred-ranking implementation + +## Product effect + +The gameplay-facing coach consumer layer is now more complete: + +- gameplay / Blueprint callers can bind defer and promote controls from a pure descriptor instead of replaying queue-state branching +- panel-widget and session-actor promote behavior no longer assumes the active queue entry is the correct promote target +- the dashboard and the gameplay-facing consumers now resolve deferred recovery targets through one shared ranking path + +## Acceptance criteria + +- shared training types expose a narrow queue-control surface +- coach-library derivation resolves the displayed defer target and the preferred deferred promote target +- panel-widget callers can fetch the displayed queue-control surface directly +- session-actor callers can fetch the displayed queue-control surface directly +- displayed promote no longer depends on `ActiveQueueEntryId` +- full product build succeeds + +## Validation checklist + +1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor` +2. confirm the new queue-control surface compiles in shared types and coach library +3. confirm panel-widget getters compile and the displayed defer/promote helpers route through the derived surface +4. confirm session-actor getters compile and the displayed defer/promote helpers route through the derived surface +5. confirm the dashboard still resolves the same deferred promote target through the shared helper + +## Validation result + +- full `Development Editor|Win64` build succeeded on `2026-05-07` +- build completed with the same preexisting `UnrealMCP` / `PythonScriptPlugin` dependency warning and `0` errors + +## Next bounded follow-on slice + +- stay on the same gameplay-facing consumer lane and expose one thin queue-recovery posture / status surface on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor`, so gameplay callers can explain why continue, defer, or promote is preferred without reading raw suppression/status strings or recovery enums directly