hypertwist/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistFirstRunLaunchWidget.cpp
2026-07-23 17:23:24 +00:00

1735 lines
57 KiB
C++

#include "HyperTwistTraining/HyperTwistFirstRunLaunchWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/ButtonSlot.h"
#include "Components/EditableTextBox.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "Components/ScrollBox.h"
#include "Components/SizeBox.h"
#include "Components/Spacer.h"
#include "Components/TextBlock.h"
#include "Components/UniformGridPanel.h"
#include "Components/UniformGridSlot.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Components/WidgetSwitcher.h"
#include "Dom/JsonObject.h"
#include "Engine/GameInstance.h"
#include "GenericPlatform/GenericPlatformHttp.h"
#include "HAL/PlatformTime.h"
#include "HAL/PlatformProcess.h"
#include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingSubsystem.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Misc/DateTime.h"
#include "Misc/Paths.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Styling/SlateBrush.h"
#include "HyperTwistUX/HyperTwistPlayerSettings.h"
#include "HyperTwistUX/HyperTwistSettingsPanelWidget.h"
namespace HyperTwistFirstRunWidgetInternal
{
const TCHAR* BrowserExperienceUrl = TEXT("https://hypertwist.app/app/browser-access");
const TCHAR* HelpCenterUrl = TEXT("https://hypertwist.app/support");
const TCHAR* BrowserPreviewActionId = TEXT("browser-preview");
const TCHAR* OllLearningDeckId = TEXT("oll-t");
const TCHAR* CrossLearningDeckId = TEXT("cross-4move");
const TCHAR* FiveStyleLearningDeckId = TEXT("5style-4mover");
const TCHAR* RouxLearningDeckId =
TEXT("roux-trainers-algorithm-case-deck-cmll-default");
const TCHAR* BlindfoldLearningDeckId =
TEXT("cube-trainer-algorithm-case-deck-commutator-uf-edge");
FLinearColor AvailabilityColor(const EHyperTwistPuzzleAvailability Availability)
{
switch (Availability)
{
case EHyperTwistPuzzleAvailability::Playable:
return FLinearColor(0.0f, 0.88f, 0.78f, 1.0f);
case EHyperTwistPuzzleAvailability::BrowserPreview:
return FLinearColor(1.0f, 0.62f, 0.22f, 1.0f);
case EHyperTwistPuzzleAvailability::LearningLibrary:
return FLinearColor(0.38f, 0.68f, 1.0f, 1.0f);
default:
return FLinearColor(0.56f, 0.62f, 0.68f, 1.0f);
}
}
UTextBlock* MakeText(
UWidgetTree* WidgetTree,
const FString& Text,
const int32 FontSize,
const FLinearColor& Color,
const FName& WidgetName = NAME_None
)
{
if (WidgetTree == nullptr)
{
return nullptr;
}
UTextBlock* TextBlock = WidgetTree->ConstructWidget<UTextBlock>(
UTextBlock::StaticClass(),
WidgetName);
TextBlock->SetText(FText::FromString(Text));
TextBlock->SetAutoWrapText(true);
TextBlock->SetColorAndOpacity(FSlateColor(Color));
FSlateFontInfo Font = TextBlock->GetFont();
Font.Size = FontSize;
TextBlock->SetFont(Font);
return TextBlock;
}
void AddVerticalText(
UVerticalBox* Parent,
UTextBlock* Text,
const FMargin& Padding = FMargin(0.0f, 2.0f)
)
{
if (Parent == nullptr || Text == nullptr)
{
return;
}
if (UVerticalBoxSlot* TextSlot = Parent->AddChildToVerticalBox(Text))
{
TextSlot->SetPadding(Padding);
}
}
}
void UHyperTwistPuzzleCardWidget::ConfigureCatalogEntry(
const FHyperTwistPuzzleCatalogEntry& InEntry
)
{
Entry = InEntry;
ActionId = Entry.LaunchRouteId.IsEmpty()
? HyperTwistFirstRunWidgetInternal::BrowserPreviewActionId
: Entry.LaunchRouteId;
EnsureWidgetTreeBuilt();
RefreshFromCatalogEntry();
}
void UHyperTwistPuzzleCardWidget::RefreshFromCatalogEntry()
{
if (AvailabilityText != nullptr)
{
AvailabilityText->SetText(FText::FromString(
FString::Printf(TEXT("%s | %s"), *Entry.Dimensionality, *Entry.AvailabilityLabel)));
AvailabilityText->SetColorAndOpacity(FSlateColor(
HyperTwistFirstRunWidgetInternal::AvailabilityColor(Entry.Availability)));
}
if (TitleText != nullptr)
{
TitleText->SetText(FText::FromString(Entry.Title));
}
if (DescriptionText != nullptr)
{
DescriptionText->SetText(FText::FromString(Entry.Description));
}
if (TagsText != nullptr)
{
TagsText->SetText(FText::FromString(FString::Join(Entry.ExperienceTags, TEXT(" / "))));
}
if (LaunchButtonText != nullptr)
{
LaunchButtonText->SetText(FText::FromString(
Entry.Availability == EHyperTwistPuzzleAvailability::Playable
? TEXT("ENTER PUZZLE")
: Entry.Availability == EHyperTwistPuzzleAvailability::BrowserPreview
? TEXT("OPEN BROWSER LAB")
: TEXT("UNAVAILABLE")));
}
if (LaunchButton != nullptr)
{
LaunchButton->SetIsEnabled(
Entry.Availability == EHyperTwistPuzzleAvailability::Playable
|| Entry.Availability == EHyperTwistPuzzleAvailability::BrowserPreview);
}
}
TSharedRef<SWidget> UHyperTwistPuzzleCardWidget::RebuildWidget()
{
Initialize();
EnsureWidgetTreeBuilt();
RefreshFromCatalogEntry();
return Super::RebuildWidget();
}
void UHyperTwistPuzzleCardWidget::EnsureWidgetTreeBuilt()
{
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
{
return;
}
UBorder* Card = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("PuzzleCard"));
Card->SetPadding(FMargin(20.0f));
Card->SetBrushColor(FLinearColor(0.034f, 0.054f, 0.078f, 0.98f));
UVerticalBox* Stack = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("PuzzleCardStack"));
Card->AddChild(Stack);
WidgetTree->RootWidget = Card;
AvailabilityText = HyperTwistFirstRunWidgetInternal::MakeText(
WidgetTree,
TEXT("PUZZLE"),
12,
FLinearColor(0.0f, 0.88f, 0.78f, 1.0f),
TEXT("PuzzleAvailabilityText"));
HyperTwistFirstRunWidgetInternal::AddVerticalText(Stack, AvailabilityText);
TitleText = HyperTwistFirstRunWidgetInternal::MakeText(
WidgetTree,
TEXT("Puzzle"),
24,
FLinearColor(0.93f, 0.97f, 1.0f, 1.0f),
TEXT("PuzzleTitleText"));
HyperTwistFirstRunWidgetInternal::AddVerticalText(
Stack,
TitleText,
FMargin(0.0f, 6.0f, 0.0f, 8.0f));
DescriptionText = HyperTwistFirstRunWidgetInternal::MakeText(
WidgetTree,
FString(),
15,
FLinearColor(0.65f, 0.75f, 0.81f, 1.0f),
TEXT("PuzzleDescriptionText"));
HyperTwistFirstRunWidgetInternal::AddVerticalText(Stack, DescriptionText);
TagsText = HyperTwistFirstRunWidgetInternal::MakeText(
WidgetTree,
FString(),
12,
FLinearColor(0.45f, 0.66f, 0.72f, 1.0f),
TEXT("PuzzleTagsText"));
HyperTwistFirstRunWidgetInternal::AddVerticalText(
Stack,
TagsText,
FMargin(0.0f, 12.0f, 0.0f, 14.0f));
LaunchButton = WidgetTree->ConstructWidget<UButton>(
UButton::StaticClass(),
TEXT("PuzzleLaunchButton"));
LaunchButton->SetBackgroundColor(FLinearColor(0.0f, 0.63f, 0.56f, 1.0f));
LaunchButton->OnClicked.AddDynamic(this, &UHyperTwistPuzzleCardWidget::HandleLaunchClicked);
LaunchButtonText = HyperTwistFirstRunWidgetInternal::MakeText(
WidgetTree,
TEXT("ENTER PUZZLE"),
13,
FLinearColor(0.005f, 0.025f, 0.026f, 1.0f),
TEXT("PuzzleLaunchButtonText"));
if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(LaunchButton->AddChild(LaunchButtonText)))
{
ButtonSlot->SetPadding(FMargin(14.0f, 9.0f));
ButtonSlot->SetHorizontalAlignment(HAlign_Center);
}
if (UVerticalBoxSlot* ButtonVerticalSlot = Stack->AddChildToVerticalBox(LaunchButton))
{
ButtonVerticalSlot->SetHorizontalAlignment(HAlign_Left);
}
}
void UHyperTwistPuzzleCardWidget::HandleLaunchClicked()
{
if (!ActionId.IsEmpty())
{
OnLaunchRequested.Broadcast(ActionId);
}
}
void UHyperTwistFirstRunLaunchWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
SetIsFocusable(true);
RebuildFirstRunLaunchSurface();
}
void UHyperTwistFirstRunLaunchWidget::NativeConstruct()
{
Super::NativeConstruct();
RebuildFirstRunLaunchSurface();
ShowPage(ActivePage);
RefreshAccountSurface();
}
void UHyperTwistFirstRunLaunchWidget::NativeDestruct()
{
if (ActiveAccountRequest.IsValid())
{
ActiveAccountRequest->OnProcessRequestComplete().Unbind();
ActiveAccountRequest->CancelRequest();
ActiveAccountRequest.Reset();
}
Super::NativeDestruct();
}
TSharedRef<SWidget> UHyperTwistFirstRunLaunchWidget::RebuildWidget()
{
Initialize();
RebuildFirstRunLaunchSurface();
return Super::RebuildWidget();
}
FReply UHyperTwistFirstRunLaunchWidget::NativeOnKeyDown(
const FGeometry& InGeometry,
const FKeyEvent& InKeyEvent
)
{
if (InKeyEvent.GetKey() == EKeys::Escape && ActivePage != EHyperTwistMainMenuPage::Home)
{
ShowPage(EHyperTwistMainMenuPage::Home);
return FReply::Handled();
}
return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}
void UHyperTwistFirstRunLaunchWidget::RebuildFirstRunLaunchSurface()
{
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
{
return;
}
UOverlay* RootOverlay = WidgetTree->ConstructWidget<UOverlay>(
UOverlay::StaticClass(),
TEXT("HyperTwistFirstRunRoot"));
WidgetTree->RootWidget = RootOverlay;
UBorder* Background = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("MainMenuBackground"));
Background->SetBrushColor(BackgroundColor);
if (UOverlaySlot* BackgroundSlot = RootOverlay->AddChildToOverlay(Background))
{
BackgroundSlot->SetHorizontalAlignment(HAlign_Fill);
BackgroundSlot->SetVerticalAlignment(VAlign_Fill);
}
UHorizontalBox* Shell = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("MainMenuShell"));
if (UOverlaySlot* ShellSlot = RootOverlay->AddChildToOverlay(Shell))
{
ShellSlot->SetPadding(FMargin(24.0f));
ShellSlot->SetHorizontalAlignment(HAlign_Fill);
ShellSlot->SetVerticalAlignment(VAlign_Fill);
}
USizeBox* NavigationSize = WidgetTree->ConstructWidget<USizeBox>(
USizeBox::StaticClass(),
TEXT("MainMenuNavigationSize"));
NavigationSize->SetWidthOverride(260.0f);
UBorder* NavigationBorder = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("MainMenuNavigationBorder"));
NavigationBorder->SetPadding(FMargin(22.0f));
NavigationBorder->SetBrushColor(PanelColor);
UVerticalBox* NavigationColumn = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("MainMenuNavigation"));
NavigationBorder->AddChild(NavigationColumn);
NavigationSize->AddChild(NavigationBorder);
if (UHorizontalBoxSlot* NavigationSlot = Shell->AddChildToHorizontalBox(NavigationSize))
{
NavigationSlot->SetPadding(FMargin(0.0f, 0.0f, 16.0f, 0.0f));
NavigationSlot->SetVerticalAlignment(VAlign_Fill);
NavigationSlot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic));
}
BuildNavigation(NavigationColumn);
UBorder* ContentBorder = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("MainMenuContentBorder"));
ContentBorder->SetPadding(FMargin(30.0f));
ContentBorder->SetBrushColor(FLinearColor(0.012f, 0.022f, 0.035f, 0.96f));
if (UHorizontalBoxSlot* ContentSlot = Shell->AddChildToHorizontalBox(ContentBorder))
{
ContentSlot->SetHorizontalAlignment(HAlign_Fill);
ContentSlot->SetVerticalAlignment(VAlign_Fill);
ContentSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
UVerticalBox* ContentStack = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("MainMenuContentStack"));
ContentBorder->AddChild(ContentStack);
CurrentPageTitle = AddTextLine(
ContentStack,
TEXT("HOME"),
12,
AccentColor,
TEXT("CurrentPageTitle"));
AddSpacer(ContentStack, 10.0f);
PageSwitcher = WidgetTree->ConstructWidget<UWidgetSwitcher>(
UWidgetSwitcher::StaticClass(),
TEXT("MainMenuPageSwitcher"));
if (UVerticalBoxSlot* SwitcherSlot = ContentStack->AddChildToVerticalBox(PageSwitcher))
{
SwitcherSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
SwitcherSlot->SetHorizontalAlignment(HAlign_Fill);
SwitcherSlot->SetVerticalAlignment(VAlign_Fill);
}
UVerticalBox* HomePage = AddPage(
TEXT("HomePage"),
TEXT("THE PUZZLE UNFOLDS"),
TEXT("Think beyond the cube"),
TEXT("A complete desktop puzzle studio for classic speedcubing, exact four-dimensional cubes from 2x2x2x2 through 6x6x6x6, the 120-cell, and five-dimensional training."));
BuildHomePage(HomePage);
UVerticalBox* PuzzlePage = AddPage(
TEXT("PuzzlesPage"),
TEXT("PUZZLE LIBRARY"),
TEXT("Choose your dimension"),
TEXT("Every playable card below resolves to an owned runtime and dedicated launch route. Start familiar, then move outward one dimension at a time."));
BuildPuzzlePage(PuzzlePage);
UVerticalBox* LearnPage = AddPage(
TEXT("LearnPage"),
TEXT("LEARNING STUDIO"),
TEXT("Practice with structure"),
TEXT("Follow guided sessions, build recognition, inspect projections, and use the coach only when you want help."));
BuildLearnPage(LearnPage);
UVerticalBox* SettingsPage = AddPage(
TEXT("SettingsPage"),
TEXT("CONTROL CENTER"),
TEXT("Make HyperTwist yours"),
TEXT("Controls, sound, graphics, accessibility, speech, and AI-provider choices persist across every puzzle."));
BuildSettingsPage(SettingsPage);
UVerticalBox* AccountPage = AddPage(
TEXT("AccountPage"),
TEXT("ACCOUNT"),
TEXT("Connect browser and desktop"),
TEXT("Use a short-lived token from your signed-in browser dashboard. HyperTwist never asks for your website password inside the game."));
BuildAccountPage(AccountPage);
UVerticalBox* AdvancedPage = AddPage(
TEXT("AdvancedPage"),
TEXT("ADVANCED"),
TEXT("Diagnostics and optional hardware"),
TEXT("Engineering views and XR checks live here so the everyday puzzle experience stays clear."));
BuildAdvancedPage(AdvancedPage);
UVerticalBox* AboutPage = AddPage(
TEXT("AboutPage"),
TEXT("ABOUT & CREDITS"),
TEXT("One studio, many carefully bounded foundations"),
TEXT("HyperTwist owns the player experience and runtime. This page records the open-source inspirations, clean-room lineages, and bounded support components behind it without presenting them as separate unfinished products."));
BuildAboutPage(AboutPage);
ShowPage(EHyperTwistMainMenuPage::Home);
}
void UHyperTwistFirstRunLaunchWidget::BuildNavigation(UVerticalBox* NavigationColumn)
{
AddTextLine(NavigationColumn, TEXT("HYPERTWIST"), 30, TextColor, TEXT("MainMenuBrand"));
AddTextLine(NavigationColumn, TEXT("HIGHER-DIMENSIONAL PUZZLE STUDIO"), 10, AccentColor);
AddSpacer(NavigationColumn, 28.0f);
UButton* HomeButton = AddNavigationButton(NavigationColumn, TEXT("Home"), TEXT("HomeNavButton"));
HomeButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowHomePage);
UButton* PuzzlesButton = AddNavigationButton(NavigationColumn, TEXT("Puzzle Library"), TEXT("PuzzlesNavButton"));
PuzzlesButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowPuzzlesPage);
UButton* LearnButton = AddNavigationButton(NavigationColumn, TEXT("Learn"), TEXT("LearnNavButton"));
LearnButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowLearnPage);
UButton* SettingsButton = AddNavigationButton(NavigationColumn, TEXT("Settings"), TEXT("SettingsNavButton"));
SettingsButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowSettingsPage);
UButton* AccountButton = AddNavigationButton(NavigationColumn, TEXT("Account"), TEXT("AccountNavButton"));
AccountButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowAccountPage);
UButton* AboutButton = AddNavigationButton(NavigationColumn, TEXT("About & Credits"), TEXT("AboutNavButton"));
AboutButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowAboutPage);
UButton* AdvancedButton = AddNavigationButton(NavigationColumn, TEXT("Advanced"), TEXT("AdvancedNavButton"));
AdvancedButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowAdvancedPage);
USpacer* FillSpacer = WidgetTree->ConstructWidget<USpacer>(USpacer::StaticClass());
if (UVerticalBoxSlot* FillSlot = NavigationColumn->AddChildToVerticalBox(FillSpacer))
{
FillSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
AddTextLine(
NavigationColumn,
TEXT("Keyboard and mouse are the complete primary path. Press Esc inside any puzzle for pause, controls, settings, or return."),
12,
MutedTextColor);
AddSpacer(NavigationColumn, 16.0f);
UButton* ExitButton = AddNavigationButton(NavigationColumn, TEXT("Exit HyperTwist"), TEXT("ExitApplicationButton"));
ExitButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::HandleExitApplication);
}
void UHyperTwistFirstRunLaunchWidget::BuildHomePage(UVerticalBox* Page)
{
AddSpacer(Page, 18.0f);
UButton* ClassicButton = AddActionButton(
Page,
TEXT("ENTER CLASSIC CUBE"),
TEXT("Start with responsive 3x3x3 free play, professional keyboard turns, scramble, timing, hints, replay, and orbit camera."),
TEXT("ClassicCubeRouteButton"),
true);
ClassicButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::OpenClassicCubeTraining);
UButton* PuzzlesButton = AddActionButton(
Page,
TEXT("EXPLORE THE PUZZLE LIBRARY"),
TEXT("Open 2x2x2x2, 3x3x3x3, 4x4x4x4, 5x5x5x5, 6x6x6x6, the 120-cell, 5D, and the browser-based MagicTile lab."),
TEXT("HomePuzzleLibraryButton"));
PuzzlesButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::ShowPuzzlesPage);
AddSpacer(Page, 28.0f);
AddFeatureCard(
Page,
TEXT("EXACT STATE"),
TEXT("Real turns, not decorative projections"),
TEXT("The generalized four-dimensional runtime owns every N^4 piece, every selectable layer, exact quarter-turn updates, projection, validation, and persistence boundaries."),
AccentColor);
AddFeatureCard(
Page,
TEXT("LEARN YOUR WAY"),
TEXT("Free play, guided practice, or focused inspection"),
TEXT("Start with established WCA controls, switch to mnemonic pairs, rebind every action, or slow the experience down and inspect one slice at a time."),
WarmAccentColor);
AddFeatureCard(
Page,
TEXT("DESKTOP FIRST"),
TEXT("The complete simulator lives here"),
TEXT("The browser lane supports account, lightweight preview, and continuity. Native HyperTwist owns the full puzzle catalog, richer rendering, offline play, input, persistence, and optional XR."),
FLinearColor(0.38f, 0.68f, 1.0f, 1.0f));
}
void UHyperTwistFirstRunLaunchWidget::BuildPuzzlePage(UVerticalBox* Page)
{
AddSpacer(Page, 20.0f);
UUniformGridPanel* Grid = WidgetTree->ConstructWidget<UUniformGridPanel>(
UUniformGridPanel::StaticClass(),
TEXT("PuzzleCatalogGrid"));
Grid->SetMinDesiredSlotWidth(360.0f);
Grid->SetSlotPadding(FMargin(8.0f));
Page->AddChildToVerticalBox(Grid);
const TArray<FHyperTwistPuzzleCatalogEntry> Catalog =
UHyperTwistFirstRunLaunchLibrary::BuildPlayerPuzzleCatalog();
for (int32 Index = 0; Index < Catalog.Num(); ++Index)
{
UHyperTwistPuzzleCardWidget* Card = WidgetTree->ConstructWidget<UHyperTwistPuzzleCardWidget>(
UHyperTwistPuzzleCardWidget::StaticClass(),
*FString::Printf(TEXT("PuzzleCatalogCard_%02d"), Index));
Card->ConfigureCatalogEntry(Catalog[Index]);
Card->OnLaunchRequested.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleCatalogLaunchRequested);
if (UUniformGridSlot* GridSlot = Grid->AddChildToUniformGrid(Card, Index / 2, Index % 2))
{
GridSlot->SetHorizontalAlignment(HAlign_Fill);
GridSlot->SetVerticalAlignment(VAlign_Fill);
}
}
}
void UHyperTwistFirstRunLaunchWidget::BuildLearnPage(UVerticalBox* Page)
{
AddSpacer(Page, 18.0f);
UButton* FollowAlongButton = AddActionButton(
Page,
TEXT("START CLASSIC FOLLOW-ALONG"),
TEXT("A guided first session with scripted scramble playback, clear next actions, and training continuity."),
TEXT("FollowAlongRouteButton"),
true);
FollowAlongButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::OpenFollowAlongTraining);
AddSpacer(Page, 22.0f);
AddTextLine(
Page,
TEXT("METHOD PROGRAMS"),
12,
AccentColor,
TEXT("LearningStudioProgramsHeading"));
AddTextLine(
Page,
TEXT("Choose a focused program, recall the case before revealing it, then grade yourself. HyperTwist records the result and advances through the deck with its retained progression policy."),
14,
MutedTextColor);
UButton* OllButton = AddActionButton(
Page,
TEXT("OLL RECOGNITION LAB"),
TEXT("Train subset-organized last-layer recognition, real setup scrambles, algorithm recall, timing, and spaced resurfacing."),
TEXT("LearningStudioOllButton"),
true);
OllButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleStartOllLearningProgram);
UButton* CrossButton = AddActionButton(
Page,
TEXT("CFOP CROSS LADDER"),
TEXT("Work through curated one-to-eight-move cross-planning tiers; this balanced entry point starts at the four-move deck."),
TEXT("LearningStudioCrossButton"));
CrossButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleStartCrossLearningProgram);
UButton* FiveStyleButton = AddActionButton(
Page,
TEXT("5-STYLE EDGE CYCLES"),
TEXT("Practice advanced mover-subset recall with hidden answers, measured response time, and durable review memory."),
TEXT("LearningStudioFiveStyleButton"));
FiveStyleButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleStartFiveStyleLearningProgram);
UButton* RouxButton = AddActionButton(
Page,
TEXT("ROUX CMLL STUDIO"),
TEXT("Study the retained CMLL default family through a clean-room, source-backed deck and the same progression engine."),
TEXT("LearningStudioRouxButton"));
RouxButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleStartRouxLearningProgram);
UButton* BlindfoldButton = AddActionButton(
Page,
TEXT("BLINDFOLD COMMUTATOR LAB"),
TEXT("Build UF-edge commutator recall with reveal-on-demand practice and persistent success/failure history."),
TEXT("LearningStudioBlindfoldButton"));
BlindfoldButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleStartBlindfoldLearningProgram);
AddSpacer(Page, 24.0f);
UBorder* ExerciseBorder = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("LearningStudioExercisePanel"));
ExerciseBorder->SetPadding(FMargin(20.0f));
ExerciseBorder->SetBrushColor(CardColor);
if (UVerticalBoxSlot* ExerciseSlot = Page->AddChildToVerticalBox(ExerciseBorder))
{
ExerciseSlot->SetPadding(FMargin(0.0f, 6.0f, 0.0f, 10.0f));
ExerciseSlot->SetHorizontalAlignment(HAlign_Fill);
}
UVerticalBox* ExerciseContent = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("LearningStudioExerciseContent"));
ExerciseBorder->AddChild(ExerciseContent);
LearningProgramText = AddTextLine(
ExerciseContent,
TEXT("SELECT A METHOD PROGRAM"),
20,
TextColor,
TEXT("LearningStudioProgram"));
LearningProgressText = AddTextLine(
ExerciseContent,
TEXT("Your attempts and next-case progression will appear here."),
12,
AccentColor,
TEXT("LearningStudioProgress"));
AddSpacer(ExerciseContent, 10.0f);
LearningPromptText = AddTextLine(
ExerciseContent,
TEXT("Prompt: no active case"),
18,
TextColor,
TEXT("LearningStudioPrompt"));
LearningSetupText = AddTextLine(
ExerciseContent,
TEXT("Setup: choose a program above to begin."),
13,
MutedTextColor,
TEXT("LearningStudioSetup"));
LearningAnswerText = AddTextLine(
ExerciseContent,
FString(),
17,
WarmAccentColor,
TEXT("LearningStudioAnswer"));
if (LearningAnswerText != nullptr)
{
LearningAnswerText->SetVisibility(ESlateVisibility::Collapsed);
}
LearningStatusText = AddTextLine(
ExerciseContent,
TEXT("Learning Studio is ready."),
13,
MutedTextColor,
TEXT("LearningStudioStatus"));
LearningRevealButton = AddActionButton(
ExerciseContent,
TEXT("REVEAL ANSWER"),
TEXT("Show the canonical algorithm and retained alternatives for the current case."),
TEXT("LearningStudioRevealButton"));
LearningRevealButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleRevealLearningAnswer);
LearningRememberedButton = AddActionButton(
ExerciseContent,
TEXT("I REMEMBERED"),
TEXT("Record a successful recall and advance using the deck selection policy."),
TEXT("LearningStudioRememberedButton"),
true);
LearningRememberedButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleLearningRemembered);
LearningReviewButton = AddActionButton(
ExerciseContent,
TEXT("REVIEW AGAIN"),
TEXT("Record a miss so this case receives higher resurfacing priority."),
TEXT("LearningStudioReviewButton"));
LearningReviewButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleLearningReviewAgain);
LearningRevealButton->SetIsEnabled(false);
LearningRememberedButton->SetIsEnabled(false);
LearningReviewButton->SetIsEnabled(false);
AddSpacer(Page, 24.0f);
AddFeatureCard(
Page,
TEXT("FOUNDATIONS AND TIMING"),
TEXT("Build fluency before speed"),
TEXT("The same training authority drives inspection, timing, replay, session history, local progression, and coach recommendations, so practice remains coherent when you return to the cube."),
AccentColor);
AddFeatureCard(
Page,
TEXT("DIMENSIONAL LADDER"),
TEXT("2x2x2x2 to 6x6x6x6"),
TEXT("Begin with the cell-first 2x projection, then advance through visible-slice modes whose layer count and state size grow while the interaction grammar remains consistent."),
WarmAccentColor);
AddFeatureCard(
Page,
TEXT("OPTIONAL COACH"),
TEXT("Help stays available, never in the way"),
TEXT("The fixed assistant panel belongs to the game interface and can use configured local or BYOK providers. Speech input and narration remain global opt-in settings, not permanent HUD clutter."),
FLinearColor(0.38f, 0.68f, 1.0f, 1.0f));
}
UHyperTwistTrainingSubsystem*
UHyperTwistFirstRunLaunchWidget::ResolveTrainingSubsystem() const
{
UGameInstance* GameInstance = GetGameInstance();
return GameInstance != nullptr
? GameInstance->GetSubsystem<UHyperTwistTrainingSubsystem>()
: nullptr;
}
void UHyperTwistFirstRunLaunchWidget::StartLearningProgram(
const FString& ProgramTitle,
const FString& DeckId
)
{
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
if (TrainingSubsystem == nullptr)
{
RefreshLearningStudioSurface(
TEXT("Learning Studio could not reach the local training service."),
true);
return;
}
FHyperTwistTrainingDeck Deck;
if (!UHyperTwistTrainingCatalogLibrary::TryFindDeckInCatalog(
UHyperTwistTrainingCatalogLibrary::MakePhase3TrainingCatalog(),
DeckId,
Deck)
|| !Deck.IsStructurallyValid())
{
RefreshLearningStudioSurface(
FString::Printf(
TEXT("%s is not available in this build's verified training catalog."),
*ProgramTitle),
true);
return;
}
EHyperTwistTrainingDeliveryMode DeliveryMode = EHyperTwistTrainingDeliveryMode::Timer;
if (!Deck.DeliveryModes.Contains(DeliveryMode))
{
DeliveryMode = Deck.DeliveryModes.Contains(EHyperTwistTrainingDeliveryMode::VirtualCube)
? EHyperTwistTrainingDeliveryMode::VirtualCube
: (Deck.DeliveryModes.IsEmpty()
? EHyperTwistTrainingDeliveryMode::Timer
: Deck.DeliveryModes[0]);
}
const FHyperTwistTrainingRunState StartedRun =
TrainingSubsystem->StartTrainingRunFromDeck(
Deck,
TEXT("local-user"),
FString::Printf(
TEXT("learning_studio_%s"),
*FGuid::NewGuid().ToString(EGuidFormats::Digits)),
DeliveryMode);
if (!StartedRun.IsStructurallyValid()
|| !StartedRun.CurrentSelection.TrainingCase.IsStructurallyValid())
{
RefreshLearningStudioSurface(
FString::Printf(TEXT("%s could not start a valid first case."), *ProgramTitle),
true);
return;
}
ActiveLearningProgramTitle = ProgramTitle;
ActiveLearningDeckId = Deck.DeckId;
bLearningAnswerRevealed = false;
LearningCaseStartedAtSeconds = FPlatformTime::Seconds();
RefreshLearningStudioSurface(
FString::Printf(TEXT("%s started. Recall the answer before revealing it."), *ProgramTitle));
}
void UHyperTwistFirstRunLaunchWidget::SubmitLearningStudioAttempt(
const bool bRemembered
)
{
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
if (TrainingSubsystem == nullptr || ActiveLearningDeckId.IsEmpty())
{
RefreshLearningStudioSurface(TEXT("Choose a method program before grading a case."), true);
return;
}
const FHyperTwistTrainingRunState CurrentRun = TrainingSubsystem->GetActiveRunState();
if (!CurrentRun.IsStructurallyValid()
|| CurrentRun.ActiveDeck.DeckId != ActiveLearningDeckId
|| !CurrentRun.CurrentSelection.TrainingCase.IsStructurallyValid())
{
RefreshLearningStudioSurface(TEXT("The active learning case is no longer available."), true);
return;
}
const int32 ElapsedMs = FMath::Max(
FMath::RoundToInt(
(FPlatformTime::Seconds() - LearningCaseStartedAtSeconds) * 1000.0),
1);
FHyperTwistTrainingAttempt Attempt;
Attempt.AttemptId = FString::Printf(
TEXT("learning_%s_%s"),
*CurrentRun.Session.TrainingSessionId,
*FGuid::NewGuid().ToString(EGuidFormats::Digits));
Attempt.TrainingSessionId = CurrentRun.Session.TrainingSessionId;
Attempt.CaseId = CurrentRun.CurrentSelection.TrainingCase.CaseId;
Attempt.Result = bRemembered
? EHyperTwistTrainingAttemptResult::Success
: EHyperTwistTrainingAttemptResult::Failure;
Attempt.TotalTimeMs = ElapsedMs;
Attempt.ExecutionTimeMs = ElapsedMs;
Attempt.Mistakes = bRemembered ? 0 : 1;
Attempt.CompletedAtUtc = FDateTime::UtcNow().ToIso8601();
const FHyperTwistTrainingRunStepResult StepResult =
TrainingSubsystem->SubmitTrainingAttempt(Attempt, ElapsedMs);
if (!StepResult.IsStructurallyValid())
{
RefreshLearningStudioSurface(TEXT("HyperTwist could not record this learning result."), true);
return;
}
bLearningAnswerRevealed = false;
LearningCaseStartedAtSeconds = FPlatformTime::Seconds();
RefreshLearningStudioSurface(
bRemembered
? TEXT("Remembered. Progress saved; the next case is ready.")
: TEXT("Marked for review. HyperTwist will resurface this case sooner."));
}
void UHyperTwistFirstRunLaunchWidget::RefreshLearningStudioSurface(
const FString& StatusMessage,
const bool bError
)
{
UHyperTwistTrainingSubsystem* TrainingSubsystem = ResolveTrainingSubsystem();
const FHyperTwistTrainingRunState RunState = TrainingSubsystem != nullptr
? TrainingSubsystem->GetActiveRunState()
: FHyperTwistTrainingRunState();
const bool bHasActiveLearningCase =
!ActiveLearningDeckId.IsEmpty()
&& RunState.IsStructurallyValid()
&& RunState.ActiveDeck.DeckId == ActiveLearningDeckId
&& RunState.CurrentSelection.TrainingCase.IsStructurallyValid();
if (LearningRevealButton != nullptr)
{
LearningRevealButton->SetIsEnabled(bHasActiveLearningCase);
}
if (LearningRememberedButton != nullptr)
{
LearningRememberedButton->SetIsEnabled(bHasActiveLearningCase);
}
if (LearningReviewButton != nullptr)
{
LearningReviewButton->SetIsEnabled(bHasActiveLearningCase);
}
if (!bHasActiveLearningCase)
{
if (LearningStatusText != nullptr && !StatusMessage.IsEmpty())
{
LearningStatusText->SetText(FText::FromString(StatusMessage));
LearningStatusText->SetColorAndOpacity(FSlateColor(
bError ? FLinearColor(1.0f, 0.36f, 0.28f, 1.0f) : MutedTextColor));
}
return;
}
const FHyperTwistTrainingCase& TrainingCase =
RunState.CurrentSelection.TrainingCase;
if (LearningProgramText != nullptr)
{
LearningProgramText->SetText(FText::FromString(
ActiveLearningProgramTitle.IsEmpty()
? RunState.ActiveDeck.Title
: ActiveLearningProgramTitle));
}
if (LearningProgressText != nullptr)
{
LearningProgressText->SetText(FText::FromString(FString::Printf(
TEXT("%s | %d attempt%s recorded | %d case%s remain in this pass"),
*RunState.ActiveDeck.Title,
RunState.Session.AttemptCount,
RunState.Session.AttemptCount == 1 ? TEXT("") : TEXT("s"),
RunState.RemainingCaseIds.Num(),
RunState.RemainingCaseIds.Num() == 1 ? TEXT("") : TEXT("s"))));
}
if (LearningPromptText != nullptr)
{
const FString PromptLabel = TrainingCase.PromptLabel.IsEmpty()
? TrainingCase.CaseId
: TrainingCase.PromptLabel;
LearningPromptText->SetText(FText::FromString(FString::Printf(
TEXT("Recall: %s"),
*PromptLabel)));
}
if (LearningSetupText != nullptr)
{
const FString SetupText = TrainingCase.ScrambleNotation.IsEmpty()
? FString::Printf(
TEXT("Focus: %s. Recall the canonical sequence before revealing it."),
TrainingCase.SubsetId.IsEmpty() ? TEXT("full case") : *TrainingCase.SubsetId)
: FString::Printf(
TEXT("Setup scramble: %s"),
*TrainingCase.ScrambleNotation);
LearningSetupText->SetText(FText::FromString(SetupText));
}
if (LearningAnswerText != nullptr)
{
FString AnswerText = TrainingCase.CanonicalNotation.IsEmpty()
? TEXT("Answer: this case uses recognition or state recall rather than fixed notation.")
: FString::Printf(TEXT("Answer: %s"), *TrainingCase.CanonicalNotation);
if (!TrainingCase.AlternateNotations.IsEmpty())
{
AnswerText += FString::Printf(
TEXT("\nAlternative: %s"),
*TrainingCase.AlternateNotations[0]);
}
LearningAnswerText->SetText(FText::FromString(AnswerText));
LearningAnswerText->SetVisibility(
bLearningAnswerRevealed
? ESlateVisibility::HitTestInvisible
: ESlateVisibility::Collapsed);
}
if (LearningStatusText != nullptr)
{
LearningStatusText->SetText(FText::FromString(
StatusMessage.IsEmpty()
? TEXT("Case ready. Reveal only after making a genuine recall attempt.")
: StatusMessage));
LearningStatusText->SetColorAndOpacity(FSlateColor(
bError ? FLinearColor(1.0f, 0.36f, 0.28f, 1.0f) : MutedTextColor));
}
}
void UHyperTwistFirstRunLaunchWidget::HandleStartOllLearningProgram()
{
StartLearningProgram(TEXT("OLL Recognition Lab"), HyperTwistFirstRunWidgetInternal::OllLearningDeckId);
}
void UHyperTwistFirstRunLaunchWidget::HandleStartCrossLearningProgram()
{
StartLearningProgram(TEXT("CFOP Cross Ladder"), HyperTwistFirstRunWidgetInternal::CrossLearningDeckId);
}
void UHyperTwistFirstRunLaunchWidget::HandleStartFiveStyleLearningProgram()
{
StartLearningProgram(TEXT("5-Style Edge Cycles"), HyperTwistFirstRunWidgetInternal::FiveStyleLearningDeckId);
}
void UHyperTwistFirstRunLaunchWidget::HandleStartRouxLearningProgram()
{
StartLearningProgram(TEXT("Roux CMLL Studio"), HyperTwistFirstRunWidgetInternal::RouxLearningDeckId);
}
void UHyperTwistFirstRunLaunchWidget::HandleStartBlindfoldLearningProgram()
{
StartLearningProgram(
TEXT("Blindfold Commutator Lab"),
HyperTwistFirstRunWidgetInternal::BlindfoldLearningDeckId);
}
void UHyperTwistFirstRunLaunchWidget::HandleRevealLearningAnswer()
{
bLearningAnswerRevealed = true;
RefreshLearningStudioSurface(
TEXT("Answer revealed. Grade the recall honestly to tune future resurfacing."));
}
void UHyperTwistFirstRunLaunchWidget::HandleLearningRemembered()
{
SubmitLearningStudioAttempt(true);
}
void UHyperTwistFirstRunLaunchWidget::HandleLearningReviewAgain()
{
SubmitLearningStudioAttempt(false);
}
void UHyperTwistFirstRunLaunchWidget::BuildSettingsPage(UVerticalBox* Page)
{
AddSpacer(Page, 16.0f);
EmbeddedSettings = WidgetTree->ConstructWidget<UHyperTwistSettingsPanelWidget>(
UHyperTwistSettingsPanelWidget::StaticClass(),
TEXT("EmbeddedSettingsPanel"));
EmbeddedSettings->bShowCloseButton = false;
if (UVerticalBoxSlot* SettingsSlot = Page->AddChildToVerticalBox(EmbeddedSettings))
{
SettingsSlot->SetHorizontalAlignment(HAlign_Fill);
SettingsSlot->SetVerticalAlignment(VAlign_Fill);
SettingsSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
EmbeddedSettings->PrepareSettingsSurface();
}
void UHyperTwistFirstRunLaunchWidget::BuildAccountPage(UVerticalBox* Page)
{
AddSpacer(Page, 18.0f);
UBorder* AccountCard = WidgetTree->ConstructWidget<UBorder>(
UBorder::StaticClass(),
TEXT("AccountStateCard"));
AccountCard->SetPadding(FMargin(20.0f));
AccountCard->SetBrushColor(CardColor);
UVerticalBox* AccountStack = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
TEXT("AccountStateStack"));
AccountCard->AddChild(AccountStack);
Page->AddChildToVerticalBox(AccountCard);
AccountIdentityText = AddTextLine(
AccountStack,
TEXT("Not linked"),
22,
TextColor,
TEXT("AccountIdentityText"));
AccountAccessText = AddTextLine(
AccountStack,
TEXT("Browser account remains the source of truth."),
14,
MutedTextColor,
TEXT("AccountAccessText"));
AccountStatusText = AddTextLine(
AccountStack,
TEXT("Open the dashboard to create a one-time link token."),
13,
AccentColor,
TEXT("AccountStatusText"));
AddSpacer(Page, 16.0f);
DesktopLinkTokenInput = WidgetTree->ConstructWidget<UEditableTextBox>(
UEditableTextBox::StaticClass(),
TEXT("DesktopLinkTokenInput"));
DesktopLinkTokenInput->SetHintText(FText::FromString(
TEXT("Paste the one-time desktop-link token or full verification URL")));
DesktopLinkTokenInput->SetForegroundColor(FLinearColor(0.05f, 0.08f, 0.10f, 1.0f));
Page->AddChildToVerticalBox(DesktopLinkTokenInput);
LinkAccountButton = AddActionButton(
Page,
TEXT("LINK THIS DESKTOP"),
TEXT("Consumes the short-lived token once and stores the returned account summary locally."),
TEXT("LinkDesktopAccountButton"),
true);
LinkAccountButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::HandleLinkAccount);
UButton* WebsiteButton = AddActionButton(
Page,
TEXT("OPEN SIGNED-IN DASHBOARD"),
TEXT("Sign in, manage billing and entitlement, generate a desktop-link token, or download a release."),
TEXT("OpenAccountWebsiteButton"));
WebsiteButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleOpenAccountWebsite);
UnlinkAccountButton = AddActionButton(
Page,
TEXT("UNLINK LOCAL ACCOUNT"),
TEXT("Clears the account summary from this Windows profile. It does not delete or sign out the browser account."),
TEXT("UnlinkDesktopAccountButton"));
UnlinkAccountButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::HandleUnlinkAccount);
RefreshAccountSurface();
}
void UHyperTwistFirstRunLaunchWidget::BuildAdvancedPage(UVerticalBox* Page)
{
AddSpacer(Page, 18.0f);
UButton* CoachButton = AddActionButton(
Page,
TEXT("OPEN ENGINEERING DASHBOARD"),
TEXT("Inspect deep runtime, bridge, validation, and training state. This is an advanced diagnostic surface, not the normal player interface."),
TEXT("CoachDashboardRouteButton"));
CoachButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::OpenCoachDashboard);
UButton* XrButton = AddActionButton(
Page,
TEXT("RUN OPTIONAL XR CHECK"),
TEXT("Starts the OpenXR validation route. Use keyboard and mouse for development until a supported headset is connected."),
TEXT("XrTrainingRouteButton"));
XrButton->OnClicked.AddDynamic(this, &UHyperTwistFirstRunLaunchWidget::OpenXrTrainingValidation);
UButton* DiagnosticsButton = AddActionButton(
Page,
TEXT("OPEN DIAGNOSTICS FOLDER"),
TEXT("Open Saved/Logs to inspect startup, runtime, and crash evidence without hunting through installation folders."),
TEXT("OpenDiagnosticsFolderButton"));
DiagnosticsButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleOpenDiagnosticsFolder);
UButton* BrowserButton = AddActionButton(
Page,
TEXT("OPEN BROWSER EXPERIENCE"),
TEXT("Launch the protected lightweight browser lane for account continuity and supported preview experiences."),
TEXT("OpenBrowserExperienceButton"));
BrowserButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleOpenBrowserExperience);
UButton* HelpButton = AddActionButton(
Page,
TEXT("OPEN HELP CENTER"),
TEXT("Read setup, control, package, account, and recovery guidance on hypertwist.app."),
TEXT("OpenHelpCenterButton"));
HelpButton->OnClicked.AddDynamic(
this,
&UHyperTwistFirstRunLaunchWidget::HandleOpenHelpCenter);
}
void UHyperTwistFirstRunLaunchWidget::BuildAboutPage(UVerticalBox* Page)
{
if (Page == nullptr)
{
return;
}
const TArray<FHyperTwistIntegratedCapability> Catalog =
UHyperTwistFirstRunLaunchLibrary::BuildIntegratedCapabilityCatalog();
int32 PermissiveCount = 0;
int32 CleanRoomCount = 0;
int32 BoundarySensitiveCount = 0;
for (const FHyperTwistIntegratedCapability& Capability : Catalog)
{
switch (Capability.LicenseLane)
{
case EHyperTwistIntegrationLicenseLane::Permissive:
++PermissiveCount;
break;
case EHyperTwistIntegrationLicenseLane::RestrictiveCleanRoom:
++CleanRoomCount;
break;
case EHyperTwistIntegrationLicenseLane::BoundarySensitive:
++BoundarySensitiveCount;
break;
default:
break;
}
}
AddSpacer(Page, 18.0f);
AddFeatureCard(
Page,
TEXT("INTEGRATION TRUTH"),
FString::Printf(TEXT("%d bounded capability lanes"), Catalog.Num()),
FString::Printf(
TEXT("%d permissive, %d restrictive clean-room, and %d boundary-sensitive lanes are represented in first-party HyperTwist owners. Playable puzzle engines are distinguished from learning services, browser support, and operator tooling."),
PermissiveCount,
CleanRoomCount,
BoundarySensitiveCount),
AccentColor);
const EHyperTwistIntegrationSurface Surfaces[] = {
EHyperTwistIntegrationSurface::NativePlayable,
EHyperTwistIntegrationSurface::NativePlayerService,
EHyperTwistIntegrationSurface::BrowserSupport,
EHyperTwistIntegrationSurface::OperatorSupport
};
for (const EHyperTwistIntegrationSurface Surface : Surfaces)
{
FString CapabilityLines;
int32 SurfaceCount = 0;
for (const FHyperTwistIntegratedCapability& Capability : Catalog)
{
if (Capability.Surface != Surface)
{
continue;
}
if (!CapabilityLines.IsEmpty())
{
CapabilityLines += TEXT("\n\n");
}
CapabilityLines += FString::Printf(
TEXT("%s\n%s\nCredit: %s (%s; %s)"),
*Capability.Title,
*Capability.Summary,
*Capability.CanonicalRepository,
*Capability.LicenseId,
*UHyperTwistFirstRunLaunchLibrary::GetIntegrationLicenseLaneLabel(
Capability.LicenseLane));
++SurfaceCount;
}
if (SurfaceCount == 0)
{
continue;
}
const FLinearColor SurfaceAccent =
Surface == EHyperTwistIntegrationSurface::NativePlayable
? AccentColor
: Surface == EHyperTwistIntegrationSurface::NativePlayerService
? FLinearColor(0.38f, 0.68f, 1.0f, 1.0f)
: Surface == EHyperTwistIntegrationSurface::BrowserSupport
? WarmAccentColor
: FLinearColor(0.76f, 0.56f, 1.0f, 1.0f);
AddFeatureCard(
Page,
FString::Printf(TEXT("%d CAPABILITIES"), SurfaceCount),
UHyperTwistFirstRunLaunchLibrary::GetIntegrationSurfaceLabel(Surface),
CapabilityLines,
SurfaceAccent);
}
AddFeatureCard(
Page,
TEXT("HOW TO READ THIS PAGE"),
TEXT("Credit is not runtime ownership"),
TEXT("Permissive lanes preserve their required notices. Restrictive lanes identify clean-room lineage only: their donor source is not part of the proprietary runtime. Boundary-sensitive lanes stay behind the documented adapter, allowlist, package, attribution, or compliance boundary."),
MutedTextColor);
}
UVerticalBox* UHyperTwistFirstRunLaunchWidget::AddPage(
const FName& PageName,
const FString& Eyebrow,
const FString& Title,
const FString& Description
)
{
if (WidgetTree == nullptr || PageSwitcher == nullptr)
{
return nullptr;
}
UScrollBox* Scroll = WidgetTree->ConstructWidget<UScrollBox>(
UScrollBox::StaticClass(),
*FString::Printf(TEXT("%sScroll"), *PageName.ToString()));
UVerticalBox* Page = WidgetTree->ConstructWidget<UVerticalBox>(
UVerticalBox::StaticClass(),
PageName);
Scroll->AddChild(Page);
PageSwitcher->AddChild(Scroll);
AddTextLine(Page, Eyebrow, 12, AccentColor);
AddTextLine(Page, Title, 38, TextColor);
AddTextLine(Page, Description, 17, MutedTextColor);
return Page;
}
void UHyperTwistFirstRunLaunchWidget::AddFeatureCard(
UVerticalBox* Parent,
const FString& Eyebrow,
const FString& Title,
const FString& Description,
const FLinearColor& Accent
) const
{
if (Parent == nullptr || WidgetTree == nullptr)
{
return;
}
UBorder* Card = WidgetTree->ConstructWidget<UBorder>(UBorder::StaticClass());
Card->SetPadding(FMargin(20.0f));
Card->SetBrushColor(CardColor);
UVerticalBox* Stack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass());
Card->AddChild(Stack);
AddTextLine(Stack, Eyebrow, 11, Accent);
AddTextLine(Stack, Title, 21, TextColor);
AddTextLine(Stack, Description, 14, MutedTextColor);
if (UVerticalBoxSlot* CardSlot = Parent->AddChildToVerticalBox(Card))
{
CardSlot->SetPadding(FMargin(0.0f, 7.0f));
}
}
void UHyperTwistFirstRunLaunchWidget::ShowPage(const EHyperTwistMainMenuPage Page)
{
ActivePage = Page;
if (PageSwitcher != nullptr)
{
PageSwitcher->SetActiveWidgetIndex(static_cast<int32>(Page));
}
if (CurrentPageTitle != nullptr)
{
const TCHAR* Label = TEXT("HOME");
switch (Page)
{
case EHyperTwistMainMenuPage::Puzzles:
Label = TEXT("PUZZLE LIBRARY");
break;
case EHyperTwistMainMenuPage::Learn:
Label = TEXT("LEARN");
break;
case EHyperTwistMainMenuPage::Settings:
Label = TEXT("SETTINGS");
break;
case EHyperTwistMainMenuPage::Account:
Label = TEXT("ACCOUNT");
break;
case EHyperTwistMainMenuPage::Advanced:
Label = TEXT("ADVANCED");
break;
case EHyperTwistMainMenuPage::About:
Label = TEXT("ABOUT & CREDITS");
break;
default:
break;
}
CurrentPageTitle->SetText(FText::FromString(Label));
}
if (Page == EHyperTwistMainMenuPage::Settings && EmbeddedSettings != nullptr)
{
EmbeddedSettings->ReloadPreferences();
}
if (Page == EHyperTwistMainMenuPage::Account)
{
RefreshAccountSurface();
}
}
bool UHyperTwistFirstRunLaunchWidget::IsFirstRunLaunchSurfaceReady() const
{
return WidgetTree != nullptr
&& WidgetTree->RootWidget != nullptr
&& WidgetTree->FindWidget(FName(TEXT("HyperTwistFirstRunRoot"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("MainMenuPageSwitcher"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("HomeNavButton"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("PuzzlesNavButton"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("AboutNavButton"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("SettingsPage"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("AboutPage"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("CoachDashboardRouteButton"))) != nullptr
&& WidgetTree->FindWidget(FName(TEXT("ClassicCubeRouteButton"))) != nullptr;
}
void UHyperTwistFirstRunLaunchWidget::ShowHomePage()
{
ShowPage(EHyperTwistMainMenuPage::Home);
}
void UHyperTwistFirstRunLaunchWidget::ShowPuzzlesPage()
{
ShowPage(EHyperTwistMainMenuPage::Puzzles);
}
void UHyperTwistFirstRunLaunchWidget::ShowLearnPage()
{
ShowPage(EHyperTwistMainMenuPage::Learn);
}
void UHyperTwistFirstRunLaunchWidget::ShowSettingsPage()
{
ShowPage(EHyperTwistMainMenuPage::Settings);
}
void UHyperTwistFirstRunLaunchWidget::ShowAccountPage()
{
ShowPage(EHyperTwistMainMenuPage::Account);
}
void UHyperTwistFirstRunLaunchWidget::ShowAdvancedPage()
{
ShowPage(EHyperTwistMainMenuPage::Advanced);
}
void UHyperTwistFirstRunLaunchWidget::ShowAboutPage()
{
ShowPage(EHyperTwistMainMenuPage::About);
}
void UHyperTwistFirstRunLaunchWidget::HandleCatalogLaunchRequested(const FString& RouteId)
{
if (RouteId == HyperTwistFirstRunWidgetInternal::BrowserPreviewActionId)
{
HandleOpenBrowserExperience();
return;
}
RequestRoute(RouteId);
}
void UHyperTwistFirstRunLaunchWidget::OpenCoachDashboard()
{
RequestRoute(TEXT("coach-dashboard"));
}
void UHyperTwistFirstRunLaunchWidget::OpenClassicCubeTraining()
{
RequestRoute(TEXT("classic-cube-training"));
}
void UHyperTwistFirstRunLaunchWidget::OpenFollowAlongTraining()
{
RequestRoute(TEXT("follow-along-training"));
}
void UHyperTwistFirstRunLaunchWidget::OpenMagic120CellTraining()
{
RequestRoute(TEXT("magic-120-cell-training"));
}
void UHyperTwistFirstRunLaunchWidget::OpenMagicCube5DTraining()
{
RequestRoute(TEXT("magic-cube-5d-training"));
}
void UHyperTwistFirstRunLaunchWidget::OpenXrTrainingValidation()
{
RequestRoute(TEXT("xr-training-validation"));
}
void UHyperTwistFirstRunLaunchWidget::RequestRoute(const FString& RouteId)
{
OnFirstRunRouteRequested.Broadcast(RouteId);
}
void UHyperTwistFirstRunLaunchWidget::HandleOpenAccountWebsite()
{
OpenExternalUrl(UHyperTwistPlayerSettingsLibrary::GetAccountWebsiteUrl());
}
void UHyperTwistFirstRunLaunchWidget::HandleOpenBrowserExperience()
{
OpenExternalUrl(HyperTwistFirstRunWidgetInternal::BrowserExperienceUrl);
}
void UHyperTwistFirstRunLaunchWidget::HandleOpenHelpCenter()
{
OpenExternalUrl(HyperTwistFirstRunWidgetInternal::HelpCenterUrl);
}
void UHyperTwistFirstRunLaunchWidget::HandleLinkAccount()
{
if (DesktopLinkTokenInput == nullptr || LinkAccountButton == nullptr)
{
return;
}
FString Token = DesktopLinkTokenInput->GetText().ToString().TrimStartAndEnd();
const int32 TokenMarker = Token.Find(TEXT("token="), ESearchCase::IgnoreCase, ESearchDir::FromStart);
if (TokenMarker != INDEX_NONE)
{
Token = Token.Mid(TokenMarker + 6);
const int32 Delimiter = Token.Find(TEXT("&"));
if (Delimiter != INDEX_NONE)
{
Token = Token.Left(Delimiter);
}
Token = FGenericPlatformHttp::UrlDecode(Token);
}
if (Token.IsEmpty() || Token.Len() > 4096)
{
RefreshAccountSurface(TEXT("Paste a valid desktop-link token from the dashboard."), true);
return;
}
if (ActiveAccountRequest.IsValid())
{
ActiveAccountRequest->OnProcessRequestComplete().Unbind();
ActiveAccountRequest->CancelRequest();
ActiveAccountRequest.Reset();
}
const FString VerifyUrl = FString::Printf(
TEXT("%s?token=%s"),
UHyperTwistPlayerSettingsLibrary::GetDesktopLinkVerifyUrl(),
*FGenericPlatformHttp::UrlEncode(Token));
ActiveAccountRequest = FHttpModule::Get().CreateRequest();
ActiveAccountRequest->SetURL(VerifyUrl);
ActiveAccountRequest->SetVerb(TEXT("GET"));
ActiveAccountRequest->SetHeader(TEXT("Accept"), TEXT("application/json"));
ActiveAccountRequest->SetTimeout(15.0f);
const TWeakObjectPtr<UHyperTwistFirstRunLaunchWidget> WeakThis(this);
ActiveAccountRequest->OnProcessRequestComplete().BindLambda(
[WeakThis](
FHttpRequestPtr CompletedRequest,
FHttpResponsePtr Response,
const bool bConnectedSuccessfully)
{
UHyperTwistFirstRunLaunchWidget* Widget = WeakThis.Get();
if (Widget == nullptr
|| Widget->ActiveAccountRequest.Get() != CompletedRequest.Get())
{
return;
}
const int32 StatusCode = Response.IsValid() ? Response->GetResponseCode() : 0;
const FString Body = Response.IsValid() ? Response->GetContentAsString() : FString();
Widget->ActiveAccountRequest.Reset();
Widget->ProcessDesktopLinkResponse(bConnectedSuccessfully, StatusCode, Body);
});
LinkAccountButton->SetIsEnabled(false);
RefreshAccountSurface(TEXT("Verifying the one-time token..."));
if (!ActiveAccountRequest->ProcessRequest())
{
ActiveAccountRequest->OnProcessRequestComplete().Unbind();
ActiveAccountRequest.Reset();
LinkAccountButton->SetIsEnabled(true);
RefreshAccountSurface(TEXT("The account verification request could not start."), true);
}
}
void UHyperTwistFirstRunLaunchWidget::ProcessDesktopLinkResponse(
const bool bConnectedSuccessfully,
const int32 StatusCode,
const FString& Body
)
{
if (LinkAccountButton != nullptr)
{
LinkAccountButton->SetIsEnabled(true);
}
if (!bConnectedSuccessfully || StatusCode < 200 || StatusCode >= 300)
{
const FString Message = StatusCode == 404
? TEXT("That token is invalid, expired, or was already used. Generate a new token in the dashboard.")
: TEXT("HyperTwist could not verify the token. Check your connection and try again.");
RefreshAccountSurface(Message, true);
return;
}
TSharedPtr<FJsonObject> Json;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Body);
bool bResponseOk = false;
if (!FJsonSerializer::Deserialize(Reader, Json)
|| !Json.IsValid()
|| !Json->TryGetBoolField(TEXT("ok"), bResponseOk)
|| !bResponseOk)
{
RefreshAccountSurface(TEXT("The account service returned an unreadable response."), true);
return;
}
FHyperTwistDesktopAccountState State;
State.bLinked = true;
if (!Json->TryGetStringField(TEXT("email"), State.Email)
|| !Json->TryGetStringField(TEXT("plan"), State.Plan)
|| !Json->TryGetStringField(TEXT("role"), State.Role)
|| !Json->TryGetStringField(TEXT("access_status"), State.AccessStatus))
{
RefreshAccountSurface(TEXT("The account response was missing required identity fields."), true);
return;
}
Json->TryGetBoolField(TEXT("can_download"), State.bCanDownload);
State.LinkedAtUtc = FDateTime::UtcNow().ToIso8601();
if (!UHyperTwistPlayerSettingsLibrary::SaveDesktopAccountState(State))
{
RefreshAccountSurface(TEXT("The account was verified but could not be stored locally."), true);
return;
}
if (DesktopLinkTokenInput != nullptr)
{
DesktopLinkTokenInput->SetText(FText::GetEmpty());
}
RefreshAccountSurface(TEXT("Desktop linked. Website billing and entitlement remain authoritative."));
}
void UHyperTwistFirstRunLaunchWidget::HandleUnlinkAccount()
{
UHyperTwistPlayerSettingsLibrary::ClearDesktopAccountState();
RefreshAccountSurface(TEXT("Local account link cleared."));
}
void UHyperTwistFirstRunLaunchWidget::RefreshAccountSurface(
const FString& StatusMessage,
const bool bError
)
{
const FHyperTwistDesktopAccountState State =
UHyperTwistPlayerSettingsLibrary::LoadDesktopAccountState();
if (AccountIdentityText != nullptr)
{
AccountIdentityText->SetText(FText::FromString(
State.bLinked ? State.Email : TEXT("Not linked")));
}
if (AccountAccessText != nullptr)
{
AccountAccessText->SetText(FText::FromString(
State.bLinked
? FString::Printf(
TEXT("Plan: %s | Role: %s | Access: %s"),
*State.Plan,
*State.Role,
*State.AccessStatus)
: TEXT("Browser account remains the source of truth for plans, billing, and downloads.")));
}
if (AccountStatusText != nullptr)
{
const FString Message = !StatusMessage.IsEmpty()
? StatusMessage
: State.bLinked
? TEXT("This desktop has an account summary. Re-link whenever the browser account changes.")
: TEXT("Open the dashboard to create a one-time link token.");
AccountStatusText->SetText(FText::FromString(Message));
AccountStatusText->SetColorAndOpacity(FSlateColor(
bError ? FLinearColor(1.0f, 0.35f, 0.28f, 1.0f) : AccentColor));
}
if (UnlinkAccountButton != nullptr)
{
UnlinkAccountButton->SetIsEnabled(State.bLinked);
}
}
void UHyperTwistFirstRunLaunchWidget::HandleOpenDiagnosticsFolder()
{
const FString LogsDirectory = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir());
FPlatformProcess::ExploreFolder(*LogsDirectory);
}
void UHyperTwistFirstRunLaunchWidget::HandleExitApplication()
{
UKismetSystemLibrary::QuitGame(
this,
GetOwningPlayer(),
EQuitPreference::Quit,
false);
}
void UHyperTwistFirstRunLaunchWidget::OpenExternalUrl(const FString& Url) const
{
if (!Url.IsEmpty())
{
FPlatformProcess::LaunchURL(*Url, nullptr, nullptr);
}
}
UTextBlock* UHyperTwistFirstRunLaunchWidget::AddTextLine(
UVerticalBox* Parent,
const FString& Text,
const int32 FontSize,
const FLinearColor& Color,
const FName& WidgetName
) const
{
UTextBlock* TextBlock = HyperTwistFirstRunWidgetInternal::MakeText(
WidgetTree,
Text,
FontSize,
Color,
WidgetName);
HyperTwistFirstRunWidgetInternal::AddVerticalText(Parent, TextBlock);
return TextBlock;
}
UButton* UHyperTwistFirstRunLaunchWidget::AddNavigationButton(
UVerticalBox* Parent,
const FString& Label,
const FName& ButtonName
) const
{
if (Parent == nullptr || WidgetTree == nullptr)
{
return nullptr;
}
UButton* Button = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), ButtonName);
Button->SetBackgroundColor(FLinearColor(0.032f, 0.058f, 0.078f, 1.0f));
UTextBlock* LabelText = HyperTwistFirstRunWidgetInternal::MakeText(
WidgetTree,
Label,
14,
TextColor);
if (UButtonSlot* LabelSlot = Cast<UButtonSlot>(Button->AddChild(LabelText)))
{
LabelSlot->SetPadding(FMargin(12.0f, 10.0f));
LabelSlot->SetHorizontalAlignment(HAlign_Left);
}
if (UVerticalBoxSlot* ButtonVerticalSlot = Parent->AddChildToVerticalBox(Button))
{
ButtonVerticalSlot->SetPadding(FMargin(0.0f, 3.0f));
ButtonVerticalSlot->SetHorizontalAlignment(HAlign_Fill);
}
return Button;
}
UButton* UHyperTwistFirstRunLaunchWidget::AddActionButton(
UVerticalBox* Parent,
const FString& Label,
const FString& SupportingText,
const FName& ButtonName,
const bool bPrimary
) const
{
if (Parent == nullptr || WidgetTree == nullptr)
{
return nullptr;
}
UButton* Button = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), ButtonName);
Button->SetBackgroundColor(
bPrimary ? FLinearColor(0.0f, 0.72f, 0.64f, 1.0f) : CardColor);
UVerticalBox* Stack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass());
if (UButtonSlot* ActionContentSlot = Cast<UButtonSlot>(Button->AddChild(Stack)))
{
ActionContentSlot->SetPadding(FMargin(18.0f, 14.0f));
}
const FLinearColor PrimaryButtonText = FLinearColor(0.004f, 0.025f, 0.026f, 1.0f);
AddTextLine(Stack, Label, 16, bPrimary ? PrimaryButtonText : TextColor);
AddTextLine(
Stack,
SupportingText,
13,
bPrimary ? FLinearColor(0.02f, 0.12f, 0.11f, 1.0f) : MutedTextColor);
if (UVerticalBoxSlot* ActionVerticalSlot = Parent->AddChildToVerticalBox(Button))
{
ActionVerticalSlot->SetPadding(FMargin(0.0f, 6.0f));
ActionVerticalSlot->SetHorizontalAlignment(HAlign_Fill);
}
return Button;
}
void UHyperTwistFirstRunLaunchWidget::AddSpacer(
UVerticalBox* Parent,
const float Height
) const
{
if (Parent == nullptr || WidgetTree == nullptr)
{
return;
}
USpacer* Spacer = WidgetTree->ConstructWidget<USpacer>(USpacer::StaticClass());
Spacer->SetSize(FVector2D(1.0f, Height));
Parent->AddChildToVerticalBox(Spacer);
}