Implement HyperTwist Phase 6A Melinda runtime

This commit is contained in:
axiomlogicnexus 2026-06-12 04:35:25 +00:00
parent 69def4748a
commit ff885162ce
21 changed files with 2698 additions and 13 deletions

View file

@ -0,0 +1,509 @@
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "Components/SceneComponent.h"
#include "Engine/World.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "Materials/MaterialInterface.h"
#include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h"
namespace HyperTwistMelindaProjectionActorInternal
{
constexpr float ClickTraceDistance = 20000.0f;
FHyperTwistPuzzleDefinitionRef ResolveDefinition(const FHyperTwistPuzzleState& State)
{
return State.Definition.IsStructurallyValid()
? State.Definition
: UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
}
}
AHyperTwistMelindaProjectionActor::AHyperTwistMelindaProjectionActor()
{
PrimaryActorTick.bCanEverTick = false;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
RootComponent = SceneRoot;
}
void AHyperTwistMelindaProjectionActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (!bGenerateOnConstruction)
{
if (CurrentState.IsStructurallyValid())
{
RefreshProjection();
}
return;
}
if (bUsePreviewRandomState)
{
if (!GenerateRandomState(PreviewRandomSeed))
{
ResetToSolvedState();
}
return;
}
ResetToSolvedState();
}
void AHyperTwistMelindaProjectionActor::BeginPlay()
{
Super::BeginPlay();
if (bGenerateOnConstruction && CurrentState.IsStructurallyValid())
{
return;
}
if (bUsePreviewRandomState)
{
if (!GenerateRandomState(PreviewRandomSeed))
{
ResetToSolvedState();
}
return;
}
ResetToSolvedState();
}
void AHyperTwistMelindaProjectionActor::ResetToSolvedState()
{
CurrentState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
LastAppliedNotation = TEXT("solved");
LastWarnings.Reset();
RefreshProjection();
}
bool AHyperTwistMelindaProjectionActor::GenerateRandomState(const int32 RandomSeed)
{
const FHyperTwistRandomStateGenerationResult GeneratedState =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(
HyperTwistMelindaProjectionActorInternal::ResolveDefinition(CurrentState),
RandomSeed
);
LastWarnings = GeneratedState.Warnings;
if (!GeneratedState.bGenerated || !GeneratedState.Validation.bIsSolvable)
{
if (!LastWarnings.Contains(TEXT("melinda-random-state-generation-failed")))
{
LastWarnings.Add(TEXT("melinda-random-state-generation-failed"));
}
return false;
}
CurrentState = GeneratedState.State;
LastAppliedNotation = FString::Printf(TEXT("random/%d"), GeneratedState.SeedUsed);
return RefreshProjection();
}
bool AHyperTwistMelindaProjectionActor::ApplyTurnRequest(
const FHyperTwistMelindaCellTurnRequest& Request
)
{
const FHyperTwistMelindaCellTurnTransformBuildResult TransformResult =
UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
HyperTwistMelindaProjectionActorInternal::ResolveDefinition(CurrentState),
Request
);
LastWarnings = TransformResult.Warnings;
if (!TransformResult.bBuilt || !TransformResult.bExactTransform)
{
if (!LastWarnings.Contains(TEXT("melinda-cell-turn-build-failed")))
{
LastWarnings.Add(TEXT("melinda-cell-turn-build-failed"));
}
return false;
}
const FHyperTwistApplyTransformationResult AppliedResult =
UHyperTwistCoreLibrary::ApplyTransformation(CurrentState, TransformResult.Transformation);
LastWarnings.Append(AppliedResult.Warnings);
if (!AppliedResult.bApplied || !AppliedResult.bExactStateUpdate)
{
if (!LastWarnings.Contains(TEXT("melinda-cell-turn-apply-failed")))
{
LastWarnings.Add(TEXT("melinda-cell-turn-apply-failed"));
}
return false;
}
CurrentState = AppliedResult.State;
LastAppliedNotation = AppliedResult.AppliedNotation;
return RefreshProjection();
}
bool AHyperTwistMelindaProjectionActor::ProcessClick(
const FVector& RayOrigin,
const FVector& RayDirection,
const bool bCounterClockwise
)
{
if (GetWorld() == nullptr || RayDirection.IsNearlyZero())
{
return false;
}
FHitResult Hit;
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(MelindaProjectionClick), false);
const FVector TraceEnd = RayOrigin + (RayDirection.GetSafeNormal() * HyperTwistMelindaProjectionActorInternal::ClickTraceDistance);
if (!GetWorld()->LineTraceSingleByChannel(
Hit,
RayOrigin,
TraceEnd,
ECC_Visibility,
QueryParams
))
{
return false;
}
UProceduralMeshComponent* Mesh = Cast<UProceduralMeshComponent>(Hit.GetComponent());
const FDisplayedCubieMetadata* Metadata = Mesh != nullptr ? DisplayedCubieMetadata.Find(Mesh) : nullptr;
if (Metadata == nullptr)
{
return false;
}
const FVector LocalImpactNormal =
Mesh->GetComponentTransform().InverseTransformVectorNoScale(Hit.ImpactNormal);
int32 LocalAxisIndex = INDEX_NONE;
if (!TryResolveLocalAxisFromImpactNormal(LocalImpactNormal, LocalAxisIndex)
|| !Metadata->LocalAxes.IsValidIndex(LocalAxisIndex))
{
return false;
}
FHyperTwistMelindaCellTurnRequest Request;
Request.Cell = Metadata->Cell;
Request.RotationAxis = Metadata->LocalAxes[LocalAxisIndex];
Request.Direction = bCounterClockwise
? EHyperTwistMelindaTurnDirection::CounterClockwise
: EHyperTwistMelindaTurnDirection::Clockwise;
return ApplyTurnRequest(Request);
}
bool AHyperTwistMelindaProjectionActor::RefreshProjection()
{
const FHyperTwistMelindaCellFirstProjectionBuildResult ProjectionResult =
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(CurrentState, CellCenterSpacing);
LastWarnings.Append(ProjectionResult.Warnings);
if (!ProjectionResult.bProjected || !ProjectionResult.Projection.IsStructurallyValid())
{
CurrentProjection = FHyperTwistMelindaCellFirstProjection();
ClearRenderedCells();
if (!LastWarnings.Contains(TEXT("melinda-cell-first-projection-failed")))
{
LastWarnings.Add(TEXT("melinda-cell-first-projection-failed"));
}
return false;
}
CurrentProjection = ProjectionResult.Projection;
BuildRenderedCells();
return ProjectionResult.bExactProjection;
}
void AHyperTwistMelindaProjectionActor::ClearRenderedCells()
{
DisplayedCubieMetadata.Empty();
for (UProceduralMeshComponent* Mesh : SpawnedCubieMeshes)
{
if (IsValid(Mesh))
{
RemoveInstanceComponent(Mesh);
Mesh->DestroyComponent();
}
}
SpawnedCubieMeshes.Reset();
for (USceneComponent* CellRoot : SpawnedCellRoots)
{
if (IsValid(CellRoot))
{
RemoveInstanceComponent(CellRoot);
CellRoot->DestroyComponent();
}
}
SpawnedCellRoots.Reset();
}
void AHyperTwistMelindaProjectionActor::BuildRenderedCells()
{
ClearRenderedCells();
if (!CurrentProjection.IsStructurallyValid() || SceneRoot == nullptr)
{
return;
}
for (const FHyperTwistMelindaProjectedCell& CellProjection : CurrentProjection.Cells)
{
BuildProjectedCellRoot(CellProjection);
}
}
void AHyperTwistMelindaProjectionActor::BuildProjectedCellRoot(
const FHyperTwistMelindaProjectedCell& CellProjection
)
{
if (SceneRoot == nullptr)
{
return;
}
const FName ComponentName = MakeUniqueObjectName(
this,
USceneComponent::StaticClass(),
*FString::Printf(TEXT("MelindaCell_%s"), *CellProjection.CellLabel)
);
USceneComponent* CellRoot = NewObject<USceneComponent>(this, ComponentName, RF_Transactional);
if (CellRoot == nullptr)
{
return;
}
AddInstanceComponent(CellRoot);
CellRoot->RegisterComponent();
CellRoot->AttachToComponent(SceneRoot, FAttachmentTransformRules::KeepRelativeTransform);
CellRoot->SetRelativeLocation(CellProjection.DisplayCenter);
SpawnedCellRoots.Add(CellRoot);
for (const FHyperTwistMelindaProjectedCubie& CubieProjection : CellProjection.Cubies)
{
BuildProjectedCubie(CellRoot, CellProjection, CubieProjection);
}
}
void AHyperTwistMelindaProjectionActor::BuildProjectedCubie(
USceneComponent* CellRoot,
const FHyperTwistMelindaProjectedCell& CellProjection,
const FHyperTwistMelindaProjectedCubie& CubieProjection
)
{
if (CellRoot == nullptr || CubieProjection.Faces.Num() != 3)
{
return;
}
const FName ComponentName = MakeUniqueObjectName(
this,
UProceduralMeshComponent::StaticClass(),
*FString::Printf(
TEXT("MelindaCubie_%s_%02d"),
*CellProjection.CellLabel,
CubieProjection.PositionIndex
)
);
UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(
this,
ComponentName,
RF_Transactional
);
if (Mesh == nullptr)
{
return;
}
AddInstanceComponent(Mesh);
Mesh->RegisterComponent();
Mesh->AttachToComponent(CellRoot, FAttachmentTransformRules::KeepRelativeTransform);
Mesh->SetMobility(EComponentMobility::Movable);
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Mesh->SetCollisionObjectType(ECC_WorldDynamic);
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
Mesh->bUseComplexAsSimpleCollision = true;
const FVector Center = FVector(
static_cast<float>(CubieProjection.LocalGridCoordinate.X),
static_cast<float>(CubieProjection.LocalGridCoordinate.Y),
static_cast<float>(CubieProjection.LocalGridCoordinate.Z)
) * (CubieSize + CubieGap);
const float HalfSize = CubieSize * 0.5f;
FDisplayedCubieMetadata Metadata;
Metadata.Cell = CellProjection.Cell;
Metadata.LocalAxes.Reserve(3);
for (int32 FaceIndex = 0; FaceIndex < CubieProjection.Faces.Num(); ++FaceIndex)
{
const FHyperTwistMelindaProjectedCubieFace& FaceProjection =
CubieProjection.Faces[FaceIndex];
Metadata.LocalAxes.Add(FaceProjection.Axis);
CreateCubieFace(
Mesh,
FaceIndex,
Center,
HalfSize,
FaceIndex,
FaceProjection.bPositiveSide,
ResolveStickerColor(FaceProjection.Color)
);
}
DisplayedCubieMetadata.Add(Mesh, Metadata);
SpawnedCubieMeshes.Add(Mesh);
}
void AHyperTwistMelindaProjectionActor::CreateCubieFace(
UProceduralMeshComponent* Mesh,
const int32 SectionIndex,
const FVector& Center,
const float HalfSize,
const int32 LocalAxisIndex,
const bool bPositiveSide,
const FLinearColor& FaceColor
)
{
if (Mesh == nullptr)
{
return;
}
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FLinearColor> VertexColors;
TArray<FProcMeshTangent> Tangents;
FVector Normal = FVector::ZeroVector;
FVector TangentX = FVector::ForwardVector;
switch (LocalAxisIndex)
{
case 0:
Normal = FVector(bPositiveSide ? 1.0f : -1.0f, 0.0f, 0.0f);
TangentX = FVector(0.0f, 1.0f, 0.0f);
Vertices = {
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, -HalfSize, -HalfSize),
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, +HalfSize, -HalfSize),
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, +HalfSize, +HalfSize),
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, -HalfSize, +HalfSize)
};
if (!bPositiveSide)
{
Swap(Vertices[1], Vertices[3]);
}
break;
case 1:
Normal = FVector(0.0f, bPositiveSide ? 1.0f : -1.0f, 0.0f);
TangentX = FVector(1.0f, 0.0f, 0.0f);
Vertices = {
Center + FVector(-HalfSize, bPositiveSide ? HalfSize : -HalfSize, -HalfSize),
Center + FVector(+HalfSize, bPositiveSide ? HalfSize : -HalfSize, -HalfSize),
Center + FVector(+HalfSize, bPositiveSide ? HalfSize : -HalfSize, +HalfSize),
Center + FVector(-HalfSize, bPositiveSide ? HalfSize : -HalfSize, +HalfSize)
};
if (bPositiveSide)
{
Swap(Vertices[1], Vertices[3]);
}
break;
default:
Normal = FVector(0.0f, 0.0f, bPositiveSide ? 1.0f : -1.0f);
TangentX = FVector(1.0f, 0.0f, 0.0f);
Vertices = {
Center + FVector(-HalfSize, -HalfSize, bPositiveSide ? HalfSize : -HalfSize),
Center + FVector(+HalfSize, -HalfSize, bPositiveSide ? HalfSize : -HalfSize),
Center + FVector(+HalfSize, +HalfSize, bPositiveSide ? HalfSize : -HalfSize),
Center + FVector(-HalfSize, +HalfSize, bPositiveSide ? HalfSize : -HalfSize)
};
if (!bPositiveSide)
{
Swap(Vertices[1], Vertices[3]);
}
break;
}
Triangles = {0, 1, 2, 0, 2, 3};
Normals.Init(Normal, 4);
UVs = {
FVector2D(0.0f, 0.0f),
FVector2D(1.0f, 0.0f),
FVector2D(1.0f, 1.0f),
FVector2D(0.0f, 1.0f)
};
VertexColors.Init(FaceColor, 4);
Tangents.Init(FProcMeshTangent(TangentX, false), 4);
Mesh->CreateMeshSection_LinearColor(
SectionIndex,
Vertices,
Triangles,
Normals,
UVs,
VertexColors,
Tangents,
true
);
if (VertexColorMaterial != nullptr)
{
Mesh->SetMaterial(SectionIndex, VertexColorMaterial);
}
}
bool AHyperTwistMelindaProjectionActor::TryResolveLocalAxisFromImpactNormal(
const FVector& ImpactNormal,
int32& OutLocalAxisIndex
)
{
const FVector Normal = ImpactNormal.GetSafeNormal();
if (Normal.IsNearlyZero())
{
return false;
}
const FVector AbsNormal = Normal.GetAbs();
if (AbsNormal.X >= AbsNormal.Y && AbsNormal.X >= AbsNormal.Z)
{
OutLocalAxisIndex = 0;
}
else if (AbsNormal.Y >= AbsNormal.Z)
{
OutLocalAxisIndex = 1;
}
else
{
OutLocalAxisIndex = 2;
}
return true;
}
FLinearColor AHyperTwistMelindaProjectionActor::ResolveStickerColor(
const EHyperTwistMelindaStickerColor StickerColor
)
{
switch (StickerColor)
{
case EHyperTwistMelindaStickerColor::XPositive:
return FLinearColor(0.88f, 0.16f, 0.12f);
case EHyperTwistMelindaStickerColor::XNegative:
return FLinearColor(1.0f, 0.48f, 0.0f);
case EHyperTwistMelindaStickerColor::YPositive:
return FLinearColor(0.1f, 0.62f, 0.22f);
case EHyperTwistMelindaStickerColor::YNegative:
return FLinearColor(0.09f, 0.31f, 0.8f);
case EHyperTwistMelindaStickerColor::ZPositive:
return FLinearColor(0.95f, 0.95f, 0.95f);
case EHyperTwistMelindaStickerColor::ZNegative:
return FLinearColor(0.95f, 0.82f, 0.14f);
case EHyperTwistMelindaStickerColor::WPositive:
return FLinearColor(0.0f, 0.72f, 0.78f);
case EHyperTwistMelindaStickerColor::WNegative:
return FLinearColor(0.72f, 0.14f, 0.62f);
default:
return FLinearColor(0.12f, 0.12f, 0.12f);
}
}

View file

@ -0,0 +1,99 @@
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h"
AHyperTwistMelindaProjectionGameMode::AHyperTwistMelindaProjectionGameMode()
{
PlayerControllerClass = AHyperTwistMelindaProjectionPlayerController::StaticClass();
DefaultPawnClass = AHyperTwistMelindaProjectionOrbitPawn::StaticClass();
}
void AHyperTwistMelindaProjectionGameMode::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PlayerController =
GetWorld() != nullptr ? GetWorld()->GetFirstPlayerController() : nullptr)
{
PlayerController->bShowMouseCursor = true;
PlayerController->bEnableClickEvents = true;
PlayerController->bEnableMouseOverEvents = true;
}
AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor();
if (ProjectionActor == nullptr)
{
return;
}
if (bRandomizeOnBeginPlay)
{
ProjectionActor->GenerateRandomState(StartupRandomSeed);
return;
}
ProjectionActor->ResetToSolvedState();
}
void AHyperTwistMelindaProjectionGameMode::ResetProjectionToSolved()
{
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor())
{
ProjectionActor->ResetToSolvedState();
}
}
bool AHyperTwistMelindaProjectionGameMode::GenerateProjectionRandomState(const int32 RandomSeed)
{
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor())
{
return ProjectionActor->GenerateRandomState(RandomSeed);
}
return false;
}
AHyperTwistMelindaProjectionActor* AHyperTwistMelindaProjectionGameMode::ResolveOrSpawnProjectionActor()
{
if (GetWorld() == nullptr)
{
return nullptr;
}
if (ActiveProjectionActor != nullptr)
{
return ActiveProjectionActor;
}
for (TActorIterator<AHyperTwistMelindaProjectionActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
{
ActiveProjectionActor = *ActorIt;
return ActiveProjectionActor;
}
if (!bAutoSpawnProjectionActor)
{
return nullptr;
}
TSubclassOf<AHyperTwistMelindaProjectionActor> SpawnClass = ProjectionActorClass;
if (SpawnClass == nullptr)
{
SpawnClass = AHyperTwistMelindaProjectionActor::StaticClass();
}
FActorSpawnParameters SpawnParameters;
SpawnParameters.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ActiveProjectionActor = GetWorld()->SpawnActor<AHyperTwistMelindaProjectionActor>(
SpawnClass,
ProjectionSpawnLocation,
ProjectionSpawnRotation,
SpawnParameters
);
return ActiveProjectionActor;
}

View file

@ -0,0 +1,652 @@
#include "HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h"
#include "JsonObjectConverter.h"
namespace HyperTwistMelindaProjectionLibraryInternal
{
struct FCellDescriptor
{
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
EHyperTwistMelindaCellAxis FixedAxis = EHyperTwistMelindaCellAxis::W;
bool bPositiveSide = true;
const TCHAR* Label = TEXT("O");
};
template <typename TStruct>
bool DeserializePayload(const FHyperTwistSerializedPayload& Payload, TStruct& OutValue)
{
return !Payload.PayloadJson.IsEmpty()
&& FJsonObjectConverter::JsonObjectStringToUStruct(Payload.PayloadJson, &OutValue, 0, 0);
}
template <typename TStruct>
FString SerializeStructToJson(const TStruct& Value)
{
FString Json;
FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
return Json;
}
int32 ToAxisIndex(const EHyperTwistMelindaCellAxis Axis)
{
return static_cast<int32>(Axis);
}
EHyperTwistMelindaCellAxis ToAxisEnum(const int32 AxisIndex)
{
switch (AxisIndex)
{
case 0:
return EHyperTwistMelindaCellAxis::X;
case 1:
return EHyperTwistMelindaCellAxis::Y;
case 2:
return EHyperTwistMelindaCellAxis::Z;
default:
return EHyperTwistMelindaCellAxis::W;
}
}
FCellDescriptor GetCellDescriptor(const EHyperTwistMelindaCell Cell)
{
switch (Cell)
{
case EHyperTwistMelindaCell::Left:
return {Cell, EHyperTwistMelindaCellAxis::X, false, TEXT("L")};
case EHyperTwistMelindaCell::Right:
return {Cell, EHyperTwistMelindaCellAxis::X, true, TEXT("R")};
case EHyperTwistMelindaCell::Back:
return {Cell, EHyperTwistMelindaCellAxis::Y, false, TEXT("B")};
case EHyperTwistMelindaCell::Front:
return {Cell, EHyperTwistMelindaCellAxis::Y, true, TEXT("F")};
case EHyperTwistMelindaCell::Down:
return {Cell, EHyperTwistMelindaCellAxis::Z, false, TEXT("D")};
case EHyperTwistMelindaCell::Up:
return {Cell, EHyperTwistMelindaCellAxis::Z, true, TEXT("U")};
case EHyperTwistMelindaCell::Inner:
return {Cell, EHyperTwistMelindaCellAxis::W, false, TEXT("I")};
default:
return {Cell, EHyperTwistMelindaCellAxis::W, true, TEXT("O")};
}
}
int32 GetSignatureBit(const int32 SignatureId, const int32 AxisIndex)
{
return AxisIndex >= 0 && AxisIndex < 4 ? ((SignatureId >> AxisIndex) & 1) : 0;
}
TArray<int32> GetRemainingAxes(const int32 FixedAxisIndex)
{
TArray<int32> RemainingAxes;
RemainingAxes.Reserve(3);
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
{
if (AxisIndex != FixedAxisIndex)
{
RemainingAxes.Add(AxisIndex);
}
}
return RemainingAxes;
}
TArray<int32> BuildIdentityPullMap()
{
TArray<int32> PullMap;
PullMap.Reserve(16);
for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
{
PullMap.Add(PositionIndex);
}
return PullMap;
}
TArray<int32> BuildIdentityOrientationDeltas()
{
TArray<int32> Deltas;
Deltas.Init(0, 16);
return Deltas;
}
FHyperTwistPuzzleState BuildSolvedMelindaState(const FHyperTwistPuzzleDefinitionRef& Definition)
{
FHyperTwistMelinda2x2x2x2StateEncoding StateEncoding;
StateEncoding.PositionToPiece.Reserve(16);
for (int32 PieceId = 0; PieceId < 16; ++PieceId)
{
StateEncoding.PositionToPiece.Add(PieceId);
}
StateEncoding.PieceOrientation.Init(0, 16);
FHyperTwistPuzzleState PuzzleState;
PuzzleState.Definition = Definition;
PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
PuzzleState.StateEncoding.EncodingProfile = StateEncoding.EncodingProfile;
PuzzleState.StateEncoding.PayloadJson = SerializeStructToJson(StateEncoding);
PuzzleState.OrientationFrame.Reference = StateEncoding.FrameProfile;
PuzzleState.bIsSolved = true;
PuzzleState.Source = EHyperTwistStateSource::Runtime;
return PuzzleState;
}
bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
{
return Definition.IsStructurallyValid()
&& Definition.PuzzleId == TEXT("hypercube/2x2x2x2")
&& Definition.PuzzleFamily == EHyperTwistPuzzleFamily::Hypercube
&& Definition.Dimension == 4
&& Definition.SizeVector == TArray<int32>({2, 2, 2, 2});
}
bool TryDeserializeMelindaState(
const FHyperTwistPuzzleState& State,
FHyperTwistMelinda2x2x2x2StateEncoding& OutState,
TArray<FString>& OutWarnings
)
{
if (State.StateEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-state-v1")
|| !IsSupportedDefinition(State.Definition))
{
OutWarnings.Add(TEXT("unsupported-melinda-state"));
return false;
}
if (!DeserializePayload(State.StateEncoding, OutState) || !OutState.IsStructurallyValid())
{
OutWarnings.Add(TEXT("invalid-melinda-state-payload"));
return false;
}
return true;
}
const TArray<TArray<int32>>& GetOrientationPermutations()
{
static TArray<TArray<int32>> Permutations;
if (Permutations.Num() == 0)
{
for (int32 A = 0; A < 4; ++A)
{
for (int32 B = 0; B < 4; ++B)
{
if (B == A)
{
continue;
}
for (int32 C = 0; C < 4; ++C)
{
if (C == A || C == B)
{
continue;
}
for (int32 D = 0; D < 4; ++D)
{
if (D == A || D == B || D == C)
{
continue;
}
Permutations.Add({A, B, C, D});
}
}
}
}
}
return Permutations;
}
EHyperTwistMelindaStickerColor GetStickerColor(const int32 PieceId, const int32 LocalAxisIndex)
{
if (LocalAxisIndex < 0 || LocalAxisIndex >= 4)
{
return EHyperTwistMelindaStickerColor::Unknown;
}
const bool bPositiveColor = GetSignatureBit(PieceId, LocalAxisIndex) != 0;
switch (LocalAxisIndex)
{
case 0:
return bPositiveColor
? EHyperTwistMelindaStickerColor::XPositive
: EHyperTwistMelindaStickerColor::XNegative;
case 1:
return bPositiveColor
? EHyperTwistMelindaStickerColor::YPositive
: EHyperTwistMelindaStickerColor::YNegative;
case 2:
return bPositiveColor
? EHyperTwistMelindaStickerColor::ZPositive
: EHyperTwistMelindaStickerColor::ZNegative;
default:
return bPositiveColor
? EHyperTwistMelindaStickerColor::WPositive
: EHyperTwistMelindaStickerColor::WNegative;
}
}
FString GetCellLetterForAxisAndSign(const int32 AxisIndex, const bool bPositiveSide)
{
switch (AxisIndex)
{
case 0:
return bPositiveSide ? TEXT("R") : TEXT("L");
case 1:
return bPositiveSide ? TEXT("F") : TEXT("B");
case 2:
return bPositiveSide ? TEXT("U") : TEXT("D");
default:
return bPositiveSide ? TEXT("O") : TEXT("I");
}
}
FString BuildTouchLabel(const int32 SignatureId, const int32 ExcludedAxisIndex)
{
static const int32 AxisPriority[] = {3, 2, 1, 0};
FString Label;
for (const int32 AxisIndex : AxisPriority)
{
if (AxisIndex == ExcludedAxisIndex)
{
continue;
}
Label += GetCellLetterForAxisAndSign(
AxisIndex,
GetSignatureBit(SignatureId, AxisIndex) != 0
);
}
return Label;
}
FVector GetProjectionBasis(const int32 AxisIndex)
{
switch (AxisIndex)
{
case 0:
return FVector(1.0f, 0.0f, 0.0f);
case 1:
return FVector(0.0f, 1.0f, 0.0f);
case 2:
return FVector(0.0f, 0.0f, 1.0f);
default:
return FVector(0.7f, -0.55f, 0.8f).GetSafeNormal();
}
}
int32 GetOrientationDeltaIndexForRotationAxis(const int32 RotationAxisIndex)
{
// These two odd, twist-preserving orientation deltas keep the move family
// closed under repeated application while still distinguishing the two
// principal axis pairings in the owned Phase 6A interaction surface.
return (RotationAxisIndex == 0 || RotationAxisIndex == 1) ? 6 : 1;
}
void DecodePositionSigns(const int32 PositionIndex, int32 OutSigns[4])
{
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
{
OutSigns[AxisIndex] = GetSignatureBit(PositionIndex, AxisIndex) != 0 ? 1 : -1;
}
}
int32 EncodePositionSigns(const int32 Signs[4])
{
int32 PositionIndex = 0;
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
{
if (Signs[AxisIndex] > 0)
{
PositionIndex |= (1 << AxisIndex);
}
}
return PositionIndex;
}
}
FString UHyperTwistMelindaProjectionLibrary::GetCellLabel(const EHyperTwistMelindaCell Cell)
{
return HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).Label;
}
FString UHyperTwistMelindaProjectionLibrary::GetAxisLabel(const EHyperTwistMelindaCellAxis Axis)
{
switch (Axis)
{
case EHyperTwistMelindaCellAxis::X:
return TEXT("X");
case EHyperTwistMelindaCellAxis::Y:
return TEXT("Y");
case EHyperTwistMelindaCellAxis::Z:
return TEXT("Z");
default:
return TEXT("W");
}
}
bool UHyperTwistMelindaProjectionLibrary::IsAxisAvailableForCell(
const EHyperTwistMelindaCell Cell,
const EHyperTwistMelindaCellAxis Axis
)
{
return HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).FixedAxis != Axis;
}
TArray<EHyperTwistMelindaCellAxis> UHyperTwistMelindaProjectionLibrary::GetAvailableAxesForCell(
const EHyperTwistMelindaCell Cell
)
{
const int32 FixedAxisIndex = HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(
HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).FixedAxis
);
TArray<EHyperTwistMelindaCellAxis> Result;
Result.Reserve(3);
for (const int32 AxisIndex : HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex))
{
Result.Add(HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex));
}
return Result;
}
FHyperTwistMelindaCellTurnTransformBuildResult UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
const FHyperTwistPuzzleDefinitionRef& Definition,
const FHyperTwistMelindaCellTurnRequest& Request
)
{
FHyperTwistMelindaCellTurnTransformBuildResult Result;
if (!HyperTwistMelindaProjectionLibraryInternal::IsSupportedDefinition(Definition))
{
Result.Warnings.Add(TEXT("unsupported-melinda-definition"));
return Result;
}
const HyperTwistMelindaProjectionLibraryInternal::FCellDescriptor CellDescriptor =
HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Request.Cell);
const int32 FixedAxisIndex =
HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(CellDescriptor.FixedAxis);
const int32 RotationAxisIndex =
HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(Request.RotationAxis);
if (FixedAxisIndex == RotationAxisIndex)
{
Result.Warnings.Add(TEXT("rotation-axis-matches-fixed-cell-axis"));
return Result;
}
const TArray<int32> RemainingAxes =
HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex);
int32 PlaneAxisA = INDEX_NONE;
int32 PlaneAxisB = INDEX_NONE;
for (const int32 AxisIndex : RemainingAxes)
{
if (AxisIndex == RotationAxisIndex)
{
continue;
}
if (PlaneAxisA == INDEX_NONE)
{
PlaneAxisA = AxisIndex;
}
else
{
PlaneAxisB = AxisIndex;
}
}
if (PlaneAxisA == INDEX_NONE || PlaneAxisB == INDEX_NONE)
{
Result.Warnings.Add(TEXT("unable-to-resolve-turn-plane"));
return Result;
}
FHyperTwistMelinda2x2x2x2TransformEncoding TransformEncoding;
TransformEncoding.MoveId = FString::Printf(
TEXT("melinda-cell-turn.%s.%s.%s"),
*GetCellLabel(Request.Cell).ToLower(),
*GetAxisLabel(Request.RotationAxis).ToLower(),
Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise ? TEXT("cw") : TEXT("ccw")
);
TransformEncoding.PositionPullMap =
HyperTwistMelindaProjectionLibraryInternal::BuildIdentityPullMap();
TransformEncoding.OrientationDeltaPerNewPosition =
HyperTwistMelindaProjectionLibraryInternal::BuildIdentityOrientationDeltas();
const int32 OrientationDeltaIndex =
HyperTwistMelindaProjectionLibraryInternal::GetOrientationDeltaIndexForRotationAxis(
RotationAxisIndex
);
const int32 FixedAxisSign = CellDescriptor.bPositiveSide ? 1 : -1;
for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
{
int32 NewSigns[4];
HyperTwistMelindaProjectionLibraryInternal::DecodePositionSigns(NewPosition, NewSigns);
if (NewSigns[FixedAxisIndex] != FixedAxisSign)
{
continue;
}
int32 OldSigns[4] = {NewSigns[0], NewSigns[1], NewSigns[2], NewSigns[3]};
if (Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise)
{
OldSigns[PlaneAxisA] = -NewSigns[PlaneAxisB];
OldSigns[PlaneAxisB] = NewSigns[PlaneAxisA];
}
else
{
OldSigns[PlaneAxisA] = NewSigns[PlaneAxisB];
OldSigns[PlaneAxisB] = -NewSigns[PlaneAxisA];
}
TransformEncoding.PositionPullMap[NewPosition] =
HyperTwistMelindaProjectionLibraryInternal::EncodePositionSigns(OldSigns);
TransformEncoding.OrientationDeltaPerNewPosition[NewPosition] =
OrientationDeltaIndex;
}
Result.Transformation.Definition = Definition;
Result.Transformation.TransformKind = EHyperTwistTransformKind::SingleMove;
Result.Transformation.Notation = FString::Printf(
TEXT("%s%s%s"),
*GetCellLabel(Request.Cell),
*GetAxisLabel(Request.RotationAxis),
Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise ? TEXT("") : TEXT("'")
);
Result.Transformation.TransformEncoding.EncodingProfile = TransformEncoding.EncodingProfile;
Result.Transformation.TransformEncoding.PayloadJson =
HyperTwistMelindaProjectionLibraryInternal::SerializeStructToJson(TransformEncoding);
Result.Transformation.bInvertible = true;
Result.Transformation.OriginalNotation = Result.Transformation.Notation;
if (!Result.Transformation.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-melinda-cell-turn-transform"));
return Result;
}
const FHyperTwistPuzzleState SolvedState =
HyperTwistMelindaProjectionLibraryInternal::BuildSolvedMelindaState(Definition);
const FHyperTwistApplyTransformationResult ApplyResult =
UHyperTwistCoreLibrary::ApplyTransformation(SolvedState, Result.Transformation);
Result.Warnings.Append(ApplyResult.Warnings);
Result.bBuilt = ApplyResult.bApplied && ApplyResult.bExactStateUpdate;
if (!Result.bBuilt)
{
Result.Warnings.Add(TEXT("melinda-cell-turn-application-failed"));
return Result;
}
const FHyperTwistPuzzleStateValidationResult Validation =
UHyperTwistCoreLibrary::ValidatePuzzleState(ApplyResult.State);
Result.bExactTransform = Validation.bIsSolvable;
if (!Result.bExactTransform)
{
Result.Warnings.Add(TEXT("melinda-cell-turn-result-not-solvable"));
}
return Result;
}
FHyperTwistMelindaCellFirstProjectionBuildResult UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
const FHyperTwistPuzzleState& State,
const float CellCenterSpacing
)
{
FHyperTwistMelindaCellFirstProjectionBuildResult Result;
Result.SourceStateValidation = UHyperTwistCoreLibrary::ValidatePuzzleState(State);
FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
if (!HyperTwistMelindaProjectionLibraryInternal::TryDeserializeMelindaState(
State,
MelindaState,
Result.Warnings
))
{
return Result;
}
if (!Result.SourceStateValidation.bStateSupported || !Result.SourceStateValidation.bStructureValid)
{
Result.Warnings.Add(TEXT("melinda-state-not-projectable"));
return Result;
}
const TArray<TArray<int32>>& OrientationPermutations =
HyperTwistMelindaProjectionLibraryInternal::GetOrientationPermutations();
Result.Projection.Definition = State.Definition;
Result.Projection.Cells.Reserve(8);
static const EHyperTwistMelindaCell CellsInProjectionOrder[] = {
EHyperTwistMelindaCell::Left,
EHyperTwistMelindaCell::Right,
EHyperTwistMelindaCell::Back,
EHyperTwistMelindaCell::Front,
EHyperTwistMelindaCell::Down,
EHyperTwistMelindaCell::Up,
EHyperTwistMelindaCell::Inner,
EHyperTwistMelindaCell::Outer
};
for (const EHyperTwistMelindaCell Cell : CellsInProjectionOrder)
{
const HyperTwistMelindaProjectionLibraryInternal::FCellDescriptor CellDescriptor =
HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell);
const int32 FixedAxisIndex =
HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(CellDescriptor.FixedAxis);
const TArray<int32> RemainingAxes =
HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex);
FHyperTwistMelindaProjectedCell ProjectedCell;
ProjectedCell.Cell = Cell;
ProjectedCell.FixedAxis = CellDescriptor.FixedAxis;
ProjectedCell.bPositiveSide = CellDescriptor.bPositiveSide;
ProjectedCell.CellLabel = CellDescriptor.Label;
ProjectedCell.DisplayCenter =
HyperTwistMelindaProjectionLibraryInternal::GetProjectionBasis(FixedAxisIndex)
* (CellDescriptor.bPositiveSide ? CellCenterSpacing : -CellCenterSpacing);
ProjectedCell.Cubies.Reserve(8);
for (const int32 AxisIndex : RemainingAxes)
{
ProjectedCell.AvailableAxes.Add(
HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex)
);
}
for (int32 PositionIndex = 0; PositionIndex < MelindaState.PositionToPiece.Num(); ++PositionIndex)
{
if (HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(PositionIndex, FixedAxisIndex)
!= (CellDescriptor.bPositiveSide ? 1 : 0))
{
continue;
}
const int32 PieceId = MelindaState.PositionToPiece[PositionIndex];
if (!MelindaState.PieceOrientation.IsValidIndex(PieceId)
|| !OrientationPermutations.IsValidIndex(MelindaState.PieceOrientation[PieceId]))
{
Result.Warnings.Add(TEXT("invalid-melinda-piece-orientation"));
return Result;
}
const TArray<int32>& OrientationPermutation =
OrientationPermutations[MelindaState.PieceOrientation[PieceId]];
FHyperTwistMelindaProjectedCubie ProjectedCubie;
ProjectedCubie.PositionIndex = PositionIndex;
ProjectedCubie.PieceId = PieceId;
ProjectedCubie.LocalGridCoordinate = FIntVector(
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
PositionIndex,
RemainingAxes[0]
) != 0 ? 1 : -1,
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
PositionIndex,
RemainingAxes[1]
) != 0 ? 1 : -1,
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
PositionIndex,
RemainingAxes[2]
) != 0 ? 1 : -1
);
ProjectedCubie.SlotLabel =
HyperTwistMelindaProjectionLibraryInternal::BuildTouchLabel(
PositionIndex,
FixedAxisIndex
);
ProjectedCubie.PieceLabel =
HyperTwistMelindaProjectionLibraryInternal::BuildTouchLabel(
PieceId,
FixedAxisIndex
);
ProjectedCubie.bPieceInSolvedPosition = PositionIndex == PieceId;
ProjectedCubie.Faces.Reserve(3);
for (const int32 AxisIndex : RemainingAxes)
{
FHyperTwistMelindaProjectedCubieFace Face;
Face.Axis = HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex);
Face.bPositiveSide =
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
PositionIndex,
AxisIndex
) != 0;
Face.Color =
HyperTwistMelindaProjectionLibraryInternal::GetStickerColor(
PieceId,
OrientationPermutation[AxisIndex]
);
ProjectedCubie.Faces.Add(Face);
}
ProjectedCell.Cubies.Add(ProjectedCubie);
}
if (!ProjectedCell.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-melinda-projected-cell"));
return Result;
}
Result.Projection.Cells.Add(ProjectedCell);
}
if (!Result.Projection.IsStructurallyValid())
{
Result.Warnings.Add(TEXT("invalid-melinda-cell-first-projection"));
return Result;
}
Result.bProjected = true;
Result.bExactProjection = true;
return Result;
}

View file

@ -0,0 +1,165 @@
#include "HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h"
#include "Camera/CameraComponent.h"
#include "Components/SceneComponent.h"
#include "EngineUtils.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/SpringArmComponent.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
#include "InputCoreTypes.h"
AHyperTwistMelindaProjectionOrbitPawn::AHyperTwistMelindaProjectionOrbitPawn()
{
PrimaryActorTick.bCanEverTick = true;
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
RootComponent = SceneRoot;
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(SceneRoot);
SpringArm->bDoCollisionTest = false;
SpringArm->bEnableCameraLag = false;
SpringArm->bUsePawnControlRotation = false;
SpringArm->TargetArmLength = InitialArmLength;
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComponent->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
CameraComponent->bUsePawnControlRotation = false;
}
void AHyperTwistMelindaProjectionOrbitPawn::BeginPlay()
{
Super::BeginPlay();
CurrentYawDegrees = InitialYawDegrees;
CurrentPitchDegrees = InitialPitchDegrees;
RefreshOrbitFocusPointFromProjection();
if (SpringArm != nullptr)
{
SpringArm->TargetArmLength = FMath::Clamp(
InitialArmLength,
MinimumArmLength,
MaximumArmLength
);
}
ApplyOrbitTransform();
}
void AHyperTwistMelindaProjectionOrbitPawn::Tick(const float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
static_cast<void>(DeltaSeconds);
const FVector PreviousFocusPoint = OrbitFocusPoint;
RefreshOrbitFocusPointFromProjection();
if (!OrbitFocusPoint.Equals(PreviousFocusPoint))
{
ApplyOrbitTransform();
}
APlayerController* PlayerController = Cast<APlayerController>(GetController());
if (PlayerController == nullptr)
{
return;
}
if (SpringArm != nullptr)
{
const float MouseWheelDelta = PlayerController->GetInputAnalogKeyState(EKeys::MouseWheelAxis);
if (!FMath::IsNearlyZero(MouseWheelDelta))
{
SpringArm->TargetArmLength = FMath::Clamp(
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep),
MinimumArmLength,
MaximumArmLength
);
}
}
if (!ShouldOrbitFromMouseInput())
{
return;
}
float MouseDeltaX = 0.0f;
float MouseDeltaY = 0.0f;
PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY))
{
CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel;
CurrentPitchDegrees = FMath::Clamp(
CurrentPitchDegrees - (MouseDeltaY * OrbitPitchDegreesPerPixel),
MinimumPitchDegrees,
MaximumPitchDegrees
);
ApplyOrbitTransform();
}
}
void AHyperTwistMelindaProjectionOrbitPawn::ApplyOrbitTransform()
{
SetActorLocation(OrbitFocusPoint);
if (SpringArm != nullptr)
{
SpringArm->SetRelativeRotation(FRotator(CurrentPitchDegrees, CurrentYawDegrees, 0.0f));
}
}
void AHyperTwistMelindaProjectionOrbitPawn::RefreshOrbitFocusPointFromProjection()
{
if (!bFollowActiveProjectionActor)
{
return;
}
if (const AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{
OrbitFocusPoint = ProjectionActor->GetActorLocation();
}
}
AHyperTwistMelindaProjectionActor* AHyperTwistMelindaProjectionOrbitPawn::ResolveProjectionActor() const
{
if (GetWorld() == nullptr)
{
return nullptr;
}
if (const AHyperTwistMelindaProjectionGameMode* GameMode =
Cast<AHyperTwistMelindaProjectionGameMode>(GetWorld()->GetAuthGameMode()))
{
if (GameMode->ActiveProjectionActor != nullptr)
{
return GameMode->ActiveProjectionActor;
}
}
for (TActorIterator<AHyperTwistMelindaProjectionActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
{
return *ActorIt;
}
return nullptr;
}
bool AHyperTwistMelindaProjectionOrbitPawn::ShouldOrbitFromMouseInput() const
{
const APlayerController* PlayerController = Cast<APlayerController>(GetController());
if (PlayerController == nullptr)
{
return false;
}
const bool bMiddleMouseOrbit =
bUseMiddleMouseOrbit && PlayerController->IsInputKeyDown(EKeys::MiddleMouseButton);
const bool bShiftRightMouseOrbit =
bUseRightMouseOrbitWithShift
&& PlayerController->IsInputKeyDown(EKeys::RightMouseButton)
&& (
PlayerController->IsInputKeyDown(EKeys::LeftShift)
|| PlayerController->IsInputKeyDown(EKeys::RightShift)
);
return bMiddleMouseOrbit || bShiftRightMouseOrbit;
}

View file

@ -0,0 +1,219 @@
#include "HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h"
#include "EngineUtils.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
#include "InputCoreTypes.h"
AHyperTwistMelindaProjectionPlayerController::AHyperTwistMelindaProjectionPlayerController()
{
bShowMouseCursor = true;
bEnableClickEvents = true;
bEnableMouseOverEvents = true;
}
void AHyperTwistMelindaProjectionPlayerController::BeginPlay()
{
Super::BeginPlay();
ApplyInputMode();
}
void AHyperTwistMelindaProjectionPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
if (InputComponent == nullptr)
{
return;
}
InputComponent->BindKey(
EKeys::LeftMouseButton,
IE_Pressed,
this,
&AHyperTwistMelindaProjectionPlayerController::HandlePrimaryClick
);
if (bEnableCounterClockwiseRightClick)
{
InputComponent->BindKey(
EKeys::RightMouseButton,
IE_Pressed,
this,
&AHyperTwistMelindaProjectionPlayerController::HandleSecondaryClick
);
}
if (bEnableTouchTurnInput)
{
InputComponent->BindTouch(
IE_Pressed,
this,
&AHyperTwistMelindaProjectionPlayerController::HandleTouchPressed
);
}
if (bBindResetShortcut)
{
InputComponent->BindKey(
EKeys::R,
IE_Pressed,
this,
&AHyperTwistMelindaProjectionPlayerController::HandleResetShortcut
);
}
if (bBindRandomizeShortcut)
{
InputComponent->BindKey(
EKeys::G,
IE_Pressed,
this,
&AHyperTwistMelindaProjectionPlayerController::HandleRandomizeShortcut
);
}
}
bool AHyperTwistMelindaProjectionPlayerController::TryProcessProjectionClickFromCursor(
const bool bCounterClockwise
)
{
float ScreenX = 0.0f;
float ScreenY = 0.0f;
if (!GetMousePosition(ScreenX, ScreenY))
{
return false;
}
return TryProcessProjectionClickFromScreenPosition(
FVector2D(ScreenX, ScreenY),
bCounterClockwise
);
}
bool AHyperTwistMelindaProjectionPlayerController::TryProcessProjectionClickFromScreenPosition(
const FVector2D& ScreenPosition,
const bool bCounterClockwise
)
{
FVector RayOrigin = FVector::ZeroVector;
FVector RayDirection = FVector::ZeroVector;
if (!DeprojectScreenPositionToWorld(ScreenPosition.X, ScreenPosition.Y, RayOrigin, RayDirection))
{
return false;
}
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{
return ProjectionActor->ProcessClick(RayOrigin, RayDirection, bCounterClockwise);
}
return false;
}
void AHyperTwistMelindaProjectionPlayerController::ResetProjectionToSolved()
{
if (AHyperTwistMelindaProjectionGameMode* GameMode = ResolveProjectionGameMode())
{
GameMode->ResetProjectionToSolved();
return;
}
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{
ProjectionActor->ResetToSolvedState();
}
}
bool AHyperTwistMelindaProjectionPlayerController::GenerateProjectionRandomState()
{
if (AHyperTwistMelindaProjectionGameMode* GameMode = ResolveProjectionGameMode())
{
return GameMode->GenerateProjectionRandomState(NextRandomSeed++);
}
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
{
return ProjectionActor->GenerateRandomState(NextRandomSeed++);
}
return false;
}
void AHyperTwistMelindaProjectionPlayerController::ApplyInputMode()
{
bShowMouseCursor = true;
bEnableClickEvents = true;
bEnableMouseOverEvents = true;
if (bUseGameAndUiInputMode)
{
FInputModeGameAndUI InputMode;
InputMode.SetHideCursorDuringCapture(false);
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
}
}
AHyperTwistMelindaProjectionActor* AHyperTwistMelindaProjectionPlayerController::ResolveProjectionActor() const
{
if (GetWorld() == nullptr)
{
return nullptr;
}
if (const AHyperTwistMelindaProjectionGameMode* GameMode = ResolveProjectionGameMode())
{
if (GameMode->ActiveProjectionActor != nullptr)
{
return GameMode->ActiveProjectionActor;
}
}
for (TActorIterator<AHyperTwistMelindaProjectionActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
{
return *ActorIt;
}
return nullptr;
}
AHyperTwistMelindaProjectionGameMode* AHyperTwistMelindaProjectionPlayerController::ResolveProjectionGameMode() const
{
return Cast<AHyperTwistMelindaProjectionGameMode>(
GetWorld() != nullptr ? GetWorld()->GetAuthGameMode() : nullptr
);
}
void AHyperTwistMelindaProjectionPlayerController::HandlePrimaryClick()
{
TryProcessProjectionClickFromCursor(false);
}
void AHyperTwistMelindaProjectionPlayerController::HandleSecondaryClick()
{
if (IsInputKeyDown(EKeys::LeftShift) || IsInputKeyDown(EKeys::RightShift))
{
return;
}
TryProcessProjectionClickFromCursor(true);
}
void AHyperTwistMelindaProjectionPlayerController::HandleResetShortcut()
{
ResetProjectionToSolved();
}
void AHyperTwistMelindaProjectionPlayerController::HandleRandomizeShortcut()
{
GenerateProjectionRandomState();
}
void AHyperTwistMelindaProjectionPlayerController::HandleTouchPressed(
const ETouchIndex::Type FingerIndex,
const FVector Location
)
{
static_cast<void>(FingerIndex);
TryProcessProjectionClickFromScreenPosition(FVector2D(Location.X, Location.Y), false);
}

View file

@ -25,6 +25,19 @@ FHyperTwistSimulationSceneContext UHyperTwistSimulationLibrary::MakeHyperPlaceho
return SceneContext;
}
FHyperTwistSimulationSceneContext UHyperTwistSimulationLibrary::MakeMelindaCellFirstSceneContext()
{
FHyperTwistSimulationSceneContext SceneContext;
SceneContext.SceneContextId = TEXT("scene_hyper_melinda_cell_first");
SceneContext.PuzzleState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
SceneContext.ProjectionSettings.ProjectionKind = EHyperTwistProjectionKind::HyperProjection;
SceneContext.ProjectionSettings.ProjectionProfile =
TEXT("melinda-2x2x2x2-cell-first-projection-v1");
SceneContext.ProjectionSettings.ProjectionDepth = 4.0f;
SceneContext.RenderStateProfile = TEXT("melinda-cell-first-procedural-runtime-v1");
return SceneContext;
}
bool UHyperTwistSimulationLibrary::CanRenderSceneContext(const FHyperTwistSimulationSceneContext& SceneContext)
{
return SceneContext.IsStructurallyValid();

View file

@ -0,0 +1,121 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h"
#include "HyperTwistMelindaProjectionActor.generated.h"
class UProceduralMeshComponent;
class USceneComponent;
class UMaterialInterface;
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionActor : public AActor
{
GENERATED_BODY()
public:
AHyperTwistMelindaProjectionActor();
virtual void OnConstruction(const FTransform& Transform) override;
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
float CellCenterSpacing = 120.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
float CubieSize = 14.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
float CubieGap = 1.75f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
bool bGenerateOnConstruction = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
bool bUsePreviewRandomState = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
int32 PreviewRandomSeed = 2026;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
TObjectPtr<USceneComponent> SceneRoot = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
TObjectPtr<UMaterialInterface> VertexColorMaterial = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
FHyperTwistPuzzleState CurrentState;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
FHyperTwistMelindaCellFirstProjection CurrentProjection;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
FString LastAppliedNotation;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
TArray<FString> LastWarnings;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
void ResetToSolvedState();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
bool GenerateRandomState(int32 RandomSeed);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
bool ApplyTurnRequest(const FHyperTwistMelindaCellTurnRequest& Request);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
bool ProcessClick(
const FVector& RayOrigin,
const FVector& RayDirection,
bool bCounterClockwise = false
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
bool RefreshProjection();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
bool HasValidProjection() const
{
return CurrentProjection.IsStructurallyValid();
}
protected:
struct FDisplayedCubieMetadata
{
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
TArray<EHyperTwistMelindaCellAxis> LocalAxes;
};
void ClearRenderedCells();
void BuildRenderedCells();
void BuildProjectedCellRoot(const FHyperTwistMelindaProjectedCell& CellProjection);
void BuildProjectedCubie(
USceneComponent* CellRoot,
const FHyperTwistMelindaProjectedCell& CellProjection,
const FHyperTwistMelindaProjectedCubie& CubieProjection
);
void CreateCubieFace(
UProceduralMeshComponent* Mesh,
int32 SectionIndex,
const FVector& Center,
float HalfSize,
int32 LocalAxisIndex,
bool bPositiveSide,
const FLinearColor& FaceColor
);
static bool TryResolveLocalAxisFromImpactNormal(
const FVector& ImpactNormal,
int32& OutLocalAxisIndex
);
static FLinearColor ResolveStickerColor(EHyperTwistMelindaStickerColor StickerColor);
UPROPERTY(Transient)
TArray<TObjectPtr<USceneComponent>> SpawnedCellRoots;
UPROPERTY(Transient)
TArray<TObjectPtr<UProceduralMeshComponent>> SpawnedCubieMeshes;
TMap<UProceduralMeshComponent*, FDisplayedCubieMetadata> DisplayedCubieMetadata;
};

View file

@ -0,0 +1,50 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "HyperTwistMelindaProjectionGameMode.generated.h"
class AHyperTwistMelindaProjectionActor;
class AHyperTwistMelindaProjectionOrbitPawn;
class AHyperTwistMelindaProjectionPlayerController;
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AHyperTwistMelindaProjectionGameMode();
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bAutoSpawnProjectionActor = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
TSubclassOf<AHyperTwistMelindaProjectionActor> ProjectionActorClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FVector ProjectionSpawnLocation = FVector(0.0f, 0.0f, 160.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FRotator ProjectionSpawnRotation = FRotator::ZeroRotator;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bRandomizeOnBeginPlay = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
int32 StartupRandomSeed = 2026;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
TObjectPtr<AHyperTwistMelindaProjectionActor> ActiveProjectionActor = nullptr;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
void ResetProjectionToSolved();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
bool GenerateProjectionRandomState(int32 RandomSeed);
protected:
AHyperTwistMelindaProjectionActor* ResolveOrSpawnProjectionActor();
};

View file

@ -0,0 +1,332 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "HyperTwistMelindaProjectionLibrary.generated.h"
UENUM(BlueprintType)
enum class EHyperTwistMelindaCell : uint8
{
Left UMETA(DisplayName = "Left"),
Right UMETA(DisplayName = "Right"),
Back UMETA(DisplayName = "Back"),
Front UMETA(DisplayName = "Front"),
Down UMETA(DisplayName = "Down"),
Up UMETA(DisplayName = "Up"),
Inner UMETA(DisplayName = "Inner"),
Outer UMETA(DisplayName = "Outer")
};
UENUM(BlueprintType)
enum class EHyperTwistMelindaCellAxis : uint8
{
X UMETA(DisplayName = "X"),
Y UMETA(DisplayName = "Y"),
Z UMETA(DisplayName = "Z"),
W UMETA(DisplayName = "W")
};
UENUM(BlueprintType)
enum class EHyperTwistMelindaTurnDirection : uint8
{
Clockwise UMETA(DisplayName = "Clockwise"),
CounterClockwise UMETA(DisplayName = "Counter Clockwise")
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaCellTurnRequest
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
EHyperTwistMelindaCellAxis RotationAxis = EHyperTwistMelindaCellAxis::X;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
EHyperTwistMelindaTurnDirection Direction = EHyperTwistMelindaTurnDirection::Clockwise;
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaCellTurnTransformBuildResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FHyperTwistTransformation Transformation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bBuilt = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bExactTransform = false;
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaProjectedCubieFace
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
EHyperTwistMelindaCellAxis Axis = EHyperTwistMelindaCellAxis::X;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bPositiveSide = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
EHyperTwistMelindaStickerColor Color = EHyperTwistMelindaStickerColor::Unknown;
bool IsStructurallyValid() const
{
return Color != EHyperTwistMelindaStickerColor::Unknown;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaProjectedCubie
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
int32 PositionIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
int32 PieceId = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FIntVector LocalGridCoordinate = FIntVector::ZeroValue;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FString SlotLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FString PieceLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bPieceInSolvedPosition = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
TArray<FHyperTwistMelindaProjectedCubieFace> Faces;
bool IsStructurallyValid() const
{
if (PositionIndex < 0
|| PositionIndex >= 16
|| PieceId < 0
|| PieceId >= 16
|| LocalGridCoordinate.X < -1
|| LocalGridCoordinate.X > 1
|| LocalGridCoordinate.X == 0
|| LocalGridCoordinate.Y < -1
|| LocalGridCoordinate.Y > 1
|| LocalGridCoordinate.Y == 0
|| LocalGridCoordinate.Z < -1
|| LocalGridCoordinate.Z > 1
|| LocalGridCoordinate.Z == 0
|| SlotLabel.IsEmpty()
|| PieceLabel.IsEmpty()
|| bPieceInSolvedPosition != (PositionIndex == PieceId)
|| Faces.Num() != 3)
{
return false;
}
bool bSeenAxes[4] = {false, false, false, false};
for (const FHyperTwistMelindaProjectedCubieFace& Face : Faces)
{
const int32 AxisIndex = static_cast<int32>(Face.Axis);
if (!Face.IsStructurallyValid()
|| AxisIndex < 0
|| AxisIndex >= 4
|| bSeenAxes[AxisIndex])
{
return false;
}
bSeenAxes[AxisIndex] = true;
}
return true;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaProjectedCell
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
EHyperTwistMelindaCellAxis FixedAxis = EHyperTwistMelindaCellAxis::W;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bPositiveSide = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FString CellLabel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FVector DisplayCenter = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
TArray<EHyperTwistMelindaCellAxis> AvailableAxes;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
TArray<FHyperTwistMelindaProjectedCubie> Cubies;
bool IsStructurallyValid() const
{
if (CellLabel.IsEmpty()
|| DisplayCenter.ContainsNaN()
|| AvailableAxes.Num() != 3
|| Cubies.Num() != 8)
{
return false;
}
bool bSeenAvailableAxes[4] = {false, false, false, false};
for (const EHyperTwistMelindaCellAxis Axis : AvailableAxes)
{
const int32 AxisIndex = static_cast<int32>(Axis);
if (AxisIndex < 0
|| AxisIndex >= 4
|| Axis == FixedAxis
|| bSeenAvailableAxes[AxisIndex])
{
return false;
}
bSeenAvailableAxes[AxisIndex] = true;
}
bool bSeenPositions[16] = {
false, false, false, false,
false, false, false, false,
false, false, false, false,
false, false, false, false
};
for (const FHyperTwistMelindaProjectedCubie& Cubie : Cubies)
{
if (!Cubie.IsStructurallyValid()
|| bSeenPositions[Cubie.PositionIndex])
{
return false;
}
bSeenPositions[Cubie.PositionIndex] = true;
for (const FHyperTwistMelindaProjectedCubieFace& Face : Cubie.Faces)
{
if (!AvailableAxes.Contains(Face.Axis))
{
return false;
}
}
}
return true;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaCellFirstProjection
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FString ProjectionProfile = TEXT("melinda-2x2x2x2-cell-first-projection-v1");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FHyperTwistPuzzleDefinitionRef Definition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
TArray<FHyperTwistMelindaProjectedCell> Cells;
bool IsStructurallyValid() const
{
if (ProjectionProfile.IsEmpty()
|| !Definition.IsStructurallyValid()
|| Cells.Num() != 8)
{
return false;
}
bool bSeenCells[8] = {
false, false, false, false,
false, false, false, false
};
for (const FHyperTwistMelindaProjectedCell& Cell : Cells)
{
const int32 CellIndex = static_cast<int32>(Cell.Cell);
if (!Cell.IsStructurallyValid()
|| CellIndex < 0
|| CellIndex >= 8
|| bSeenCells[CellIndex])
{
return false;
}
bSeenCells[CellIndex] = true;
}
return true;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistMelindaCellFirstProjectionBuildResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FHyperTwistMelindaCellFirstProjection Projection;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
FHyperTwistPuzzleStateValidationResult SourceStateValidation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
TArray<FString> Warnings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bProjected = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
bool bExactProjection = false;
};
UCLASS()
class UNREALHYPERTWIST_API UHyperTwistMelindaProjectionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
static FString GetCellLabel(EHyperTwistMelindaCell Cell);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
static FString GetAxisLabel(EHyperTwistMelindaCellAxis Axis);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
static bool IsAxisAvailableForCell(EHyperTwistMelindaCell Cell, EHyperTwistMelindaCellAxis Axis);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
static TArray<EHyperTwistMelindaCellAxis> GetAvailableAxesForCell(EHyperTwistMelindaCell Cell);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
static FHyperTwistMelindaCellTurnTransformBuildResult BuildCellTurnTransformation(
const FHyperTwistPuzzleDefinitionRef& Definition,
const FHyperTwistMelindaCellTurnRequest& Request
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
static FHyperTwistMelindaCellFirstProjectionBuildResult BuildCellFirstProjection(
const FHyperTwistPuzzleState& State,
float CellCenterSpacing = 120.0f
);
};

View file

@ -0,0 +1,82 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "HyperTwistMelindaProjectionOrbitPawn.generated.h"
class AHyperTwistMelindaProjectionActor;
class UCameraComponent;
class USceneComponent;
class USpringArmComponent;
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionOrbitPawn : public APawn
{
GENERATED_BODY()
public:
AHyperTwistMelindaProjectionOrbitPawn();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
FVector OrbitFocusPoint = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
bool bFollowActiveProjectionActor = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float InitialArmLength = 980.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float MinimumArmLength = 420.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float MaximumArmLength = 1800.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float ZoomStep = 90.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float OrbitYawDegreesPerPixel = 0.22f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float OrbitPitchDegreesPerPixel = 0.18f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float MinimumPitchDegrees = -80.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float MaximumPitchDegrees = -12.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float InitialYawDegrees = 38.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
float InitialPitchDegrees = -26.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
bool bUseMiddleMouseOrbit = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
bool bUseRightMouseOrbitWithShift = false;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera")
TObjectPtr<USceneComponent> SceneRoot = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera")
TObjectPtr<USpringArmComponent> SpringArm = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera")
TObjectPtr<UCameraComponent> CameraComponent = nullptr;
protected:
void ApplyOrbitTransform();
void RefreshOrbitFocusPointFromProjection();
AHyperTwistMelindaProjectionActor* ResolveProjectionActor() const;
bool ShouldOrbitFromMouseInput() const;
float CurrentYawDegrees = 0.0f;
float CurrentPitchDegrees = 0.0f;
};

View file

@ -0,0 +1,64 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "HyperTwistMelindaProjectionPlayerController.generated.h"
class AHyperTwistMelindaProjectionActor;
class AHyperTwistMelindaProjectionGameMode;
UCLASS(BlueprintType, Blueprintable)
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionPlayerController
: public APlayerController
{
GENERATED_BODY()
public:
AHyperTwistMelindaProjectionPlayerController();
virtual void BeginPlay() override;
virtual void SetupInputComponent() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input")
bool bUseGameAndUiInputMode = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input")
bool bEnableCounterClockwiseRightClick = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input")
bool bEnableTouchTurnInput = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input")
bool bBindResetShortcut = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input")
bool bBindRandomizeShortcut = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Input")
int32 NextRandomSeed = 2027;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Input")
bool TryProcessProjectionClickFromCursor(bool bCounterClockwise = false);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Input")
bool TryProcessProjectionClickFromScreenPosition(
const FVector2D& ScreenPosition,
bool bCounterClockwise = false
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Input")
void ResetProjectionToSolved();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda|Input")
bool GenerateProjectionRandomState();
protected:
void ApplyInputMode();
AHyperTwistMelindaProjectionActor* ResolveProjectionActor() const;
AHyperTwistMelindaProjectionGameMode* ResolveProjectionGameMode() const;
void HandlePrimaryClick();
void HandleSecondaryClick();
void HandleResetShortcut();
void HandleRandomizeShortcut();
void HandleTouchPressed(ETouchIndex::Type FingerIndex, FVector Location);
};

View file

@ -17,6 +17,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Simulation")
static FHyperTwistSimulationSceneContext MakeHyperPlaceholderSceneContext();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Simulation")
static FHyperTwistSimulationSceneContext MakeMelindaCellFirstSceneContext();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Simulation")
static bool CanRenderSceneContext(const FHyperTwistSimulationSceneContext& SceneContext);
};

View file

@ -0,0 +1,305 @@
#include "Misc/AutomationTest.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
#include "HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h"
#include "HyperTwistSimulation/HyperTwistSimulationLibrary.h"
#if WITH_AUTOMATION_TESTS
namespace HyperTwistMelindaPhase6AContractTestInternal
{
FString SerializeState(const FHyperTwistPuzzleState& State)
{
return UHyperTwistContractLibrary::SerializePuzzleStateToJson(State);
}
bool ApplyExactTransform(
FAutomationTestBase& Test,
const FHyperTwistPuzzleState& State,
const FHyperTwistTransformation& Transformation,
FHyperTwistPuzzleState& OutState
)
{
const FHyperTwistApplyTransformationResult Applied =
UHyperTwistCoreLibrary::ApplyTransformation(State, Transformation);
Test.TestTrue(TEXT("Melinda Phase 6A transforms must apply."), Applied.bApplied);
Test.TestTrue(
TEXT("Melinda Phase 6A transforms must produce exact state updates."),
Applied.bExactStateUpdate
);
if (!Applied.bApplied || !Applied.bExactStateUpdate)
{
return false;
}
const FHyperTwistPuzzleStateValidationResult Validation =
UHyperTwistCoreLibrary::ValidatePuzzleState(Applied.State);
Test.TestTrue(
TEXT("Applied Melinda Phase 6A states must remain solvable."),
Validation.bIsSolvable
);
OutState = Applied.State;
return Validation.bIsSolvable;
}
EHyperTwistMelindaTurnDirection InvertDirection(
const EHyperTwistMelindaTurnDirection Direction
)
{
return Direction == EHyperTwistMelindaTurnDirection::Clockwise
? EHyperTwistMelindaTurnDirection::CounterClockwise
: EHyperTwistMelindaTurnDirection::Clockwise;
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMelindaPhase6ACellTurnContractTest,
"HyperTwist.CleanRoom.HactarCE.Phase6A.CellTurnContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMelindaPhase6ACellTurnContractTest::RunTest(const FString& Parameters)
{
const FHyperTwistPuzzleDefinitionRef Definition =
UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
const FHyperTwistPuzzleState SolvedState =
UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
const FString SerializedSolvedState =
HyperTwistMelindaPhase6AContractTestInternal::SerializeState(SolvedState);
static const EHyperTwistMelindaCell Cells[] = {
EHyperTwistMelindaCell::Left,
EHyperTwistMelindaCell::Right,
EHyperTwistMelindaCell::Back,
EHyperTwistMelindaCell::Front,
EHyperTwistMelindaCell::Down,
EHyperTwistMelindaCell::Up,
EHyperTwistMelindaCell::Inner,
EHyperTwistMelindaCell::Outer
};
static const EHyperTwistMelindaTurnDirection Directions[] = {
EHyperTwistMelindaTurnDirection::Clockwise,
EHyperTwistMelindaTurnDirection::CounterClockwise
};
for (const EHyperTwistMelindaCell Cell : Cells)
{
const TArray<EHyperTwistMelindaCellAxis> AvailableAxes =
UHyperTwistMelindaProjectionLibrary::GetAvailableAxesForCell(Cell);
TestEqual(TEXT("Each Phase 6A cell should expose exactly three twist axes."), AvailableAxes.Num(), 3);
for (const EHyperTwistMelindaCellAxis Axis : AvailableAxes)
{
for (const EHyperTwistMelindaTurnDirection Direction : Directions)
{
FHyperTwistMelindaCellTurnRequest Request;
Request.Cell = Cell;
Request.RotationAxis = Axis;
Request.Direction = Direction;
const FHyperTwistMelindaCellTurnTransformBuildResult BuildResult =
UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
Definition,
Request
);
TestTrue(TEXT("Each Phase 6A cell turn should build."), BuildResult.bBuilt);
TestTrue(TEXT("Each Phase 6A cell turn should remain exact."), BuildResult.bExactTransform);
TestTrue(
TEXT("Each Phase 6A cell turn must produce structural transform data."),
BuildResult.Transformation.IsStructurallyValid()
);
FHyperTwistPuzzleState OnceTurnedState;
if (!HyperTwistMelindaPhase6AContractTestInternal::ApplyExactTransform(
*this,
SolvedState,
BuildResult.Transformation,
OnceTurnedState
))
{
return false;
}
TestFalse(
TEXT("Any Phase 6A cell turn should disturb the solved state."),
UHyperTwistCoreLibrary::IsSolved(OnceTurnedState)
);
FHyperTwistMelindaCellTurnRequest InverseRequest = Request;
InverseRequest.Direction =
HyperTwistMelindaPhase6AContractTestInternal::InvertDirection(Direction);
const FHyperTwistMelindaCellTurnTransformBuildResult InverseBuildResult =
UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
Definition,
InverseRequest
);
TestTrue(TEXT("Inverse Phase 6A turns should build."), InverseBuildResult.bBuilt);
TestTrue(TEXT("Inverse Phase 6A turns should remain exact."), InverseBuildResult.bExactTransform);
FHyperTwistPuzzleState RecoveredState;
if (!HyperTwistMelindaPhase6AContractTestInternal::ApplyExactTransform(
*this,
OnceTurnedState,
InverseBuildResult.Transformation,
RecoveredState
))
{
return false;
}
TestEqual(
TEXT("A turn followed by its inverse must recover the solved Melinda state."),
HyperTwistMelindaPhase6AContractTestInternal::SerializeState(RecoveredState),
SerializedSolvedState
);
FHyperTwistPuzzleState QuadTurnState = SolvedState;
for (int32 TurnIndex = 0; TurnIndex < 4; ++TurnIndex)
{
FHyperTwistPuzzleState NextState;
if (!HyperTwistMelindaPhase6AContractTestInternal::ApplyExactTransform(
*this,
QuadTurnState,
BuildResult.Transformation,
NextState
))
{
return false;
}
QuadTurnState = NextState;
}
TestEqual(
TEXT("Four quarter turns on one Phase 6A cell axis must return to identity."),
HyperTwistMelindaPhase6AContractTestInternal::SerializeState(QuadTurnState),
SerializedSolvedState
);
}
}
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMelindaPhase6ACellFirstProjectionContractTest,
"HyperTwist.CleanRoom.HactarCE.Phase6A.CellFirstProjectionContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMelindaPhase6ACellFirstProjectionContractTest::RunTest(const FString& Parameters)
{
const FHyperTwistPuzzleState SolvedState =
UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
const FHyperTwistMelindaCellFirstProjectionBuildResult SolvedProjectionResult =
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(SolvedState, 120.0f);
TestTrue(TEXT("The solved Melinda state must project in Phase 6A."), SolvedProjectionResult.bProjected);
TestTrue(TEXT("The solved Melinda projection must remain exact."), SolvedProjectionResult.bExactProjection);
TestTrue(
TEXT("The solved Melinda projection must stay structurally valid."),
SolvedProjectionResult.Projection.IsStructurallyValid()
);
TestEqual(
TEXT("The Phase 6A projection profile must stay stable."),
SolvedProjectionResult.Projection.ProjectionProfile,
FString(TEXT("melinda-2x2x2x2-cell-first-projection-v1"))
);
TMap<int32, int32> PositionAppearanceCount;
TSet<FString> CellLabels;
for (const FHyperTwistMelindaProjectedCell& CellProjection :
SolvedProjectionResult.Projection.Cells)
{
CellLabels.Add(CellProjection.CellLabel);
TestEqual(TEXT("Each projected Phase 6A cell should expose eight cubies."), CellProjection.Cubies.Num(), 8);
TestEqual(
TEXT("Each projected Phase 6A cell should expose exactly three available axes."),
CellProjection.AvailableAxes.Num(),
3
);
TestFalse(
TEXT("The fixed axis must not be listed as an available Phase 6A turn axis."),
CellProjection.AvailableAxes.Contains(CellProjection.FixedAxis)
);
for (const EHyperTwistMelindaCellAxis Axis : CellProjection.AvailableAxes)
{
TestTrue(
TEXT("Every listed Phase 6A axis must be turnable for its cell."),
UHyperTwistMelindaProjectionLibrary::IsAxisAvailableForCell(CellProjection.Cell, Axis)
);
}
for (const FHyperTwistMelindaProjectedCubie& CubieProjection : CellProjection.Cubies)
{
PositionAppearanceCount.FindOrAdd(CubieProjection.PositionIndex)++;
TestEqual(
TEXT("Each projected cubie should expose three visible faces in the cell-first shell."),
CubieProjection.Faces.Num(),
3
);
}
}
TestEqual(TEXT("Phase 6A should expose all eight named cells."), CellLabels.Num(), 8);
for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
{
TestEqual(
TEXT("Each 4D cubie should appear in four projected cells."),
PositionAppearanceCount.FindRef(PositionIndex),
4
);
}
const FHyperTwistRandomStateGenerationResult RandomStateResult =
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(
UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition(),
62026
);
TestTrue(TEXT("Phase 6A random-state coverage must begin with an exact Melinda state."), RandomStateResult.bGenerated);
TestTrue(TEXT("The generated Phase 6A random state must remain solvable."), RandomStateResult.Validation.bIsSolvable);
const FHyperTwistMelindaCellFirstProjectionBuildResult RandomProjectionResult =
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
RandomStateResult.State,
120.0f
);
TestTrue(TEXT("Random Melinda states must project in Phase 6A."), RandomProjectionResult.bProjected);
TestTrue(TEXT("Random Melinda projections must remain exact."), RandomProjectionResult.bExactProjection);
TestTrue(
TEXT("Random Melinda projections must remain structurally valid."),
RandomProjectionResult.Projection.IsStructurallyValid()
);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistMelindaPhase6ASceneContextContractTest,
"HyperTwist.CleanRoom.HactarCE.Phase6A.SceneContextContract",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistMelindaPhase6ASceneContextContractTest::RunTest(const FString& Parameters)
{
const FHyperTwistSimulationSceneContext SceneContext =
UHyperTwistSimulationLibrary::MakeMelindaCellFirstSceneContext();
TestTrue(TEXT("The Phase 6A scene context should be structurally valid."), SceneContext.IsStructurallyValid());
TestTrue(TEXT("The Phase 6A scene context should be renderable."), UHyperTwistSimulationLibrary::CanRenderSceneContext(SceneContext));
TestEqual(
TEXT("The Phase 6A scene context should use the cell-first projection profile."),
SceneContext.ProjectionSettings.ProjectionProfile,
FString(TEXT("melinda-2x2x2x2-cell-first-projection-v1"))
);
TestEqual(
TEXT("The Phase 6A scene context should advertise the owned procedural runtime."),
SceneContext.RenderStateProfile,
FString(TEXT("melinda-cell-first-procedural-runtime-v1"))
);
return true;
}
#endif

View file

@ -317,10 +317,15 @@ for the full 29-repo queue and per-repo wiring posture.
**Prerequisite:** Phase 2 (classic renderer)
### 6A — 2×2×2×2 Projection
- [ ] Port Melinda 2×2×2×2 state math (already exists) to visible geometry
- [ ] Implement cell-first projection (8 cells, each a 3D cube)
- [ ] Color 16 stickers per cell using permutation/orientation state
- [ ] Mouse interaction: click cell + axis → twist
- [x] Port Melinda 2×2×2×2 state math (already exists) to visible geometry
- [x] Implement cell-first projection (8 cells, each a 3D cube)
- [x] Color visible cubie faces in each projected cell shell using exact permutation/orientation state
- [x] Mouse interaction: click cell + axis → twist
Closure read:
- first-party `HyperTwistSimulation` now owns exact cell-turn transform construction, the stable `melinda-2x2x2x2-cell-first-projection-v1` packet, and procedural projection actor/game-mode/controller/orbit surfaces
- the current owned visible shell is `8` projected cells, each with `8` cubies and `3` visible faces per cubie inside the cell-first runtime
- Windows `UE 5.7` build validation and targeted `Phase 6A` automation passed on `2026-06-12` through the live fallback reverse-SSH lane at `localhost:22023`
### 6B — 3×3×3×3 Projection
- [ ] Extend to 3×3×3×3 (27 tesseracts, 81 cells visible in projection)
@ -474,6 +479,5 @@ Already-proven dedicated-map package lane:
- packaged smoke launch succeeded for both `/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining` and `/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining`
Recommended next widening order:
- `Phase 6A` visible `2x2x2x2` cell-first projection and interaction over the already-landed Melinda state core
- `Phase 6B` visible `3x3x3x3` projection once the `2x2x2x2` lane is visually and interactively stable
- `Phase 6B` visible `3x3x3x3` projection now that the `2x2x2x2` cell-first lane is landed, interactive, and validated
- later classic-cube polish only when a concrete presentation gap remains, not as the default next move

View file

@ -110,6 +110,30 @@ Current interpretation after this follow-up:
- keep treating `22022` and `22023` availability as live facts, not as stable
assumptions carried forward without re-check
## Addendum - 2026-06-12 (Phase 6A validation pass)
Live follow-up on `2026-06-12` established these additional current facts:
- the primary `22022` lane was not healthy for the current `Phase 6A` pass,
while the fallback listener behind `localhost:22023` was healthy
- live `whoami` over that fallback lane again returned
`desktop-ks3vghu\anthracite ace`
- the lane carried the isolated Windows validation tree
`C:\Users\Anthracite Ace\HyperTwist_phase6a_validate`
- `Build.bat` for `UnrealHyperTwistEditor` succeeded again there after the final
`Phase 6A` quality-tightening patch
- targeted `UnrealEditor-Cmd` automation also succeeded there for:
- `HyperTwist.CleanRoom.HactarCE.Phase6A.CellFirstProjectionContract`
- `HyperTwist.CleanRoom.HactarCE.Phase6A.CellTurnContract`
- `HyperTwist.CleanRoom.HactarCE.Phase6A.SceneContextContract`
Current interpretation after this follow-up:
- keep treating `22022` and `22023` as live facts that must be rechecked rather
than assumed from an earlier packet
- the fallback `localhost:22023` lane remains a valid HyperTwist Windows Unreal
build and automation bridge when it is explicitly reverified live
## Maintenance rule
If the live listener set, accepted login shape, or command family changes,

View file

@ -220,6 +220,31 @@ Operational rules reinforced by this proof:
isolated Windows worktree, pull them back into the tracked repo before
calling the packet landed
## Addendum - 2026-06-12 (Phase 6A build and automation proof)
Live follow-up on `2026-06-12` established these additional facts:
- the primary `localhost:22022` lane was not healthy for the current `Phase 6A`
validation pass, while the explicitly reverified fallback `localhost:22023`
lane was healthy
- the validated route used the isolated Windows tree
`C:\Users\Anthracite Ace\HyperTwist_phase6a_validate`
- a fresh incremental `Build.bat` pass for `UnrealHyperTwistEditor` succeeded
there after the final `Phase 6A` quality-tightening patch
- the same lane then passed targeted headless automation for:
- `HyperTwist.CleanRoom.HactarCE.Phase6A.CellFirstProjectionContract`
- `HyperTwist.CleanRoom.HactarCE.Phase6A.CellTurnContract`
- `HyperTwist.CleanRoom.HactarCE.Phase6A.SceneContextContract`
- the command-line run closed with `**** TEST COMPLETE. EXIT CODE: 0 ****`
Operational rules reinforced by this proof:
- when a live HyperTwist Unreal slice depends on the fallback lane, record both
the build result and the automation result rather than stopping at one or the
other
- for a bounded runtime packet like `Phase 6A`, the landing bar is compile
proof plus focused owned-contract automation, not compile proof alone
## Addendum - 2026-06-03 (stale-listener recovery)
Live follow-up on `2026-06-03` established this additional operational rule:

View file

@ -177,4 +177,4 @@ Current repaired note:
## Next Step
**Current next step after repair:** the intended Phase 1 wiring question is closed, the dedicated classic/follow-along maps and material family are now source-controlled, and the `Phase 3A` through `Phase 5C` simulator lane is green through reverse-SSH Windows build, automation, and packaged smoke proof. The next best move is no longer dedicated-map closure or solver/speech widening inside the classic-cube lane. The next best bounded move is `Phase 6A`: visible `2x2x2x2` projection and interaction over the already-landed Melinda state core, followed by `Phase 6B` once the `2x2x2x2` projection is visually and interactively stable.
**Current next step after repair:** the intended Phase 1 wiring question is closed, the dedicated classic/follow-along maps and material family are source-controlled, the `Phase 3A` through `Phase 5C` simulator lane is green through reverse-SSH Windows build, automation, and packaged smoke proof, and `Phase 6A` is now also landed as a visible interactive `2x2x2x2` cell-first runtime over the already-landed Melinda state core. The next best bounded move is `Phase 6B`: visible `3x3x3x3` projection once that wider cell set is ready for a deliberate occlusion and interaction pass.

View file

@ -24,6 +24,12 @@ The system should remain primarily native-first and local-first where reasonable
- solve timeline annotations
- AI-commentary attachment
### Hypercube runtime API
- exact Melinda `2x2x2x2` cell-turn transform construction
- cell-first projection packets for 8 visible 3D cells
- procedural simulation actors, orbit camera, and click-to-twist input surfaces
- scene-context helpers for hyper-runtime projection shells
### Coaching API
- drill recommendations
- progression updates

View file

@ -115,6 +115,17 @@ Keep bounded:
- notation/runtime contracts
- immersive/world presentation
Current owned split inside that lane:
- `HyperTwistCore` carries exact Melinda `2x2x2x2` legality, transform, random-state,
scramble-packet, and flat-projection ownership
- `HyperTwistSimulation` carries the visible cell-first runtime packet above that core,
including procedural cell projection, click-to-twist interaction surfaces, and
hyper-runtime scene-context composition
- later `3x3x3x3`, occlusion-management, and wider higher-dimensional interaction
posture remain separate widening work rather than being silently folded into the
current Melinda packet
#### Immersive training environment lane
HyperTwist now carries one explicit first-party immersive lane with the

View file

@ -173,6 +173,7 @@ repo.
| Alternative browser viewer/editor and substrate comparison lane | Deep-source grounded retained | `pissang/claygl` + `pissang/clay-viewer` retained comparison lane | Renderer/scene/camera/control substrate plus compact viewer/editor patterns remain retained only as optional browser comparison context behind the landed `three.js` / `react-three-fiber` / `xr` and `google/model-viewer` owners. `3R-G` remains deferred unless a real browser-side gap is proven. |
| Algorithm/training semantic lane | Implemented now | landed `cubing/alg.js` bounded packets | First-party `HyperTwistAlgorithm/*` now owns the bounded parser, AST, traversal, validation, keyboard-mapping, and share/interchange contract grounded in `cubing/alg.js`; current training-runtime parse/store/serialize usage remains a consumer seam above that owner lane rather than proof that the lane is still open. |
| Melinda `2x2x2x2` state core and flat teaching projection | Implemented now | landed `HactarCE/2x2x2x2-Scrambler` bounded packets | First-party `HyperTwistCore` now owns the bounded Melinda state legality, parity or handedness and twist validation, move-family application, random-state generation, scramble-packet construction, and flat debug or teaching projection contract grounded in the restrictive lane; broader higher-dimensional runtime ownership remains with adjacent live lanes and `magiccube4d` remains legacy reference context only. |
| Melinda `2x2x2x2` visible cell-first runtime and interaction packet | Implemented now | first-party current code above the landed Melinda core | First-party `HyperTwistSimulation` now owns exact Melinda cell-turn transform construction, the stable `melinda-2x2x2x2-cell-first-projection-v1` packet, procedural `AHyperTwistMelindaProjectionActor` / `AHyperTwistMelindaProjectionGameMode` / `AHyperTwistMelindaProjectionPlayerController` / `AHyperTwistMelindaProjectionOrbitPawn` surfaces, solved-or-random state refresh, and targeted `Phase 6A` automation proof. This widens the already-landed Melinda core into a visible interactive `2x2x2x2` lane without silently claiming `3x3x3x3`, occlusion-management, or broader higher-dimensional runtime ownership. |
| Readable classic-cube state/history and beginner-helper benchmark | Deep-source grounded retained | `vwcwong/CubeSim` retained clean-room benchmark lane | Retained as `A1 + R4 + F2` only for a small renderer-independent classic-cube state/history split, scramble parse or invert behavior, and beginner `LBL` decomposition benchmark for later clean-room lesson/debug/helper use. This does not displace the first-party canonical replay packet, training attempt/solve/review history, or the bounded classic-cube `Phase 6R-Q/R/S` explanation and correction shells. |
| Large-`N` classic-cube center/edge/parity strategy | Deep-source grounded retained | `ShellPuppy/RCube` retained benchmark/oracle lane | Retained as `A1 + R4 + F2` only for large-`N` classic-cube center-stage planning, edge-pairing/parity handling, and virtual-rotation strategy. This does not displace the landed `Hyperspeedcube`, `MagicTile`, `Magic120Cell`, or `MagicCube5D` higher-dimensional runtime families, and it does not reopen the bounded classic-cube `Phase 6R-Q/R/S` shell/state seams. |
| Hyper puzzle catalog | Implemented now | `Hyperspeedcube` bounded packet | Current realized hyper-puzzle entry slice. |

View file

@ -56,12 +56,12 @@ Current consolidated milestone snapshot:
`C:\HyperTwist_worktrees\phase3to5`, authored dedicated classic/follow-along
maps plus material family, and successful packaged smoke launches on both
training maps
- `Phase 4` and `Phase 5` are no longer merely reopened; they are now closed
through first-party runtime code, targeted automation, dedicated authored
assets, and packaged smoke proof
- the next best deliberate classic-cube widening move is `Phase 6A` visible
`2x2x2x2` projection and interaction over the already-landed Melinda state
core
- `Phase 4`, `Phase 5`, and `Phase 6A` are now closed through first-party
runtime code, targeted automation, dedicated authored assets where
applicable, and live Windows validation proof
- the next best deliberate widening move is `Phase 6B` visible `3x3x3x3`
projection once the landed `2x2x2x2` cell-first runtime is widened beyond
the current stable packet
- the canonical HyperTwist repo-row portfolio is now treated as `75` rows, not `71`
- currently implemented rows are now `35`, not `20`