Expose gameplay queue-control surface
This commit is contained in:
parent
440cf06173
commit
8ffee266a4
10 changed files with 412 additions and 38 deletions
|
|
@ -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<float>::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
|
||||
|
|
|
|||
|
|
@ -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<float>::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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue