diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionActor.cpp new file mode 100644 index 0000000..c8d2906 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionActor.cpp @@ -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(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(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(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( + 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(CubieProjection.LocalGridCoordinate.X), + static_cast(CubieProjection.LocalGridCoordinate.Y), + static_cast(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 Vertices; + TArray Triangles; + TArray Normals; + TArray UVs; + TArray VertexColors; + TArray 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); + } +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.cpp new file mode 100644 index 0000000..5ebfd95 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.cpp @@ -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 ActorIt(GetWorld()); ActorIt; ++ActorIt) + { + ActiveProjectionActor = *ActorIt; + return ActiveProjectionActor; + } + + if (!bAutoSpawnProjectionActor) + { + return nullptr; + } + + TSubclassOf SpawnClass = ProjectionActorClass; + if (SpawnClass == nullptr) + { + SpawnClass = AHyperTwistMelindaProjectionActor::StaticClass(); + } + + FActorSpawnParameters SpawnParameters; + SpawnParameters.SpawnCollisionHandlingOverride = + ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + ActiveProjectionActor = GetWorld()->SpawnActor( + SpawnClass, + ProjectionSpawnLocation, + ProjectionSpawnRotation, + SpawnParameters + ); + return ActiveProjectionActor; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.cpp new file mode 100644 index 0000000..2d5babd --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.cpp @@ -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 + bool DeserializePayload(const FHyperTwistSerializedPayload& Payload, TStruct& OutValue) + { + return !Payload.PayloadJson.IsEmpty() + && FJsonObjectConverter::JsonObjectStringToUStruct(Payload.PayloadJson, &OutValue, 0, 0); + } + + template + 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(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 GetRemainingAxes(const int32 FixedAxisIndex) + { + TArray RemainingAxes; + RemainingAxes.Reserve(3); + for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex) + { + if (AxisIndex != FixedAxisIndex) + { + RemainingAxes.Add(AxisIndex); + } + } + + return RemainingAxes; + } + + TArray BuildIdentityPullMap() + { + TArray PullMap; + PullMap.Reserve(16); + for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex) + { + PullMap.Add(PositionIndex); + } + + return PullMap; + } + + TArray BuildIdentityOrientationDeltas() + { + TArray 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({2, 2, 2, 2}); + } + + bool TryDeserializeMelindaState( + const FHyperTwistPuzzleState& State, + FHyperTwistMelinda2x2x2x2StateEncoding& OutState, + TArray& 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>& GetOrientationPermutations() + { + static TArray> 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 UHyperTwistMelindaProjectionLibrary::GetAvailableAxesForCell( + const EHyperTwistMelindaCell Cell +) +{ + const int32 FixedAxisIndex = HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex( + HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).FixedAxis + ); + + TArray 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 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>& 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 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& 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; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.cpp new file mode 100644 index 0000000..5a4fcdb --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.cpp @@ -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(TEXT("SceneRoot")); + RootComponent = SceneRoot; + + SpringArm = CreateDefaultSubobject(TEXT("SpringArm")); + SpringArm->SetupAttachment(SceneRoot); + SpringArm->bDoCollisionTest = false; + SpringArm->bEnableCameraLag = false; + SpringArm->bUsePawnControlRotation = false; + SpringArm->TargetArmLength = InitialArmLength; + + CameraComponent = CreateDefaultSubobject(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(DeltaSeconds); + + const FVector PreviousFocusPoint = OrbitFocusPoint; + RefreshOrbitFocusPointFromProjection(); + if (!OrbitFocusPoint.Equals(PreviousFocusPoint)) + { + ApplyOrbitTransform(); + } + + APlayerController* PlayerController = Cast(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(GetWorld()->GetAuthGameMode())) + { + if (GameMode->ActiveProjectionActor != nullptr) + { + return GameMode->ActiveProjectionActor; + } + } + + for (TActorIterator ActorIt(GetWorld()); ActorIt; ++ActorIt) + { + return *ActorIt; + } + + return nullptr; +} + +bool AHyperTwistMelindaProjectionOrbitPawn::ShouldOrbitFromMouseInput() const +{ + const APlayerController* PlayerController = Cast(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; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.cpp new file mode 100644 index 0000000..74cff61 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.cpp @@ -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 ActorIt(GetWorld()); ActorIt; ++ActorIt) + { + return *ActorIt; + } + + return nullptr; +} + +AHyperTwistMelindaProjectionGameMode* AHyperTwistMelindaProjectionPlayerController::ResolveProjectionGameMode() const +{ + return Cast( + 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(FingerIndex); + TryProcessProjectionClickFromScreenPosition(FVector2D(Location.X, Location.Y), false); +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistSimulationLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistSimulationLibrary.cpp index 486bbc5..b9197be 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistSimulationLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistSimulationLibrary.cpp @@ -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(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionActor.h new file mode 100644 index 0000000..ebdb244 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionActor.h @@ -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 SceneRoot = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering") + TObjectPtr 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 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 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> SpawnedCellRoots; + + UPROPERTY(Transient) + TArray> SpawnedCubieMeshes; + + TMap DisplayedCubieMetadata; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h new file mode 100644 index 0000000..a5a5ec4 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h @@ -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 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 ActiveProjectionActor = nullptr; + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda") + void ResetProjectionToSolved(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda") + bool GenerateProjectionRandomState(int32 RandomSeed); + +protected: + AHyperTwistMelindaProjectionActor* ResolveOrSpawnProjectionActor(); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h new file mode 100644 index 0000000..31e6b41 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h @@ -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 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 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(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 AvailableAxes; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda") + TArray 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(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 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(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 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 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 + ); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h new file mode 100644 index 0000000..44a82c0 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h @@ -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 SceneRoot = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera") + TObjectPtr SpringArm = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera") + TObjectPtr CameraComponent = nullptr; + +protected: + void ApplyOrbitTransform(); + void RefreshOrbitFocusPointFromProjection(); + AHyperTwistMelindaProjectionActor* ResolveProjectionActor() const; + bool ShouldOrbitFromMouseInput() const; + + float CurrentYawDegrees = 0.0f; + float CurrentPitchDegrees = 0.0f; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h new file mode 100644 index 0000000..daac66a --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h @@ -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); +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationLibrary.h index d6ef3dd..5bb9987 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistSimulationLibrary.h @@ -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); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistMelindaPhase6AContractTest.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistMelindaPhase6AContractTest.cpp new file mode 100644 index 0000000..b997ab0 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Tests/HyperTwistMelindaPhase6AContractTest.cpp @@ -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 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 PositionAppearanceCount; + TSet 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 diff --git a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md index df80181..9b4a39c 100644 --- a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md +++ b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md @@ -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 diff --git a/docs/ops/HYPERTWIST_REVERSE_SSH_LIVE_CONNECTION_FACTS_2026-06-04.md b/docs/ops/HYPERTWIST_REVERSE_SSH_LIVE_CONNECTION_FACTS_2026-06-04.md index 2471a35..e84d26f 100644 --- a/docs/ops/HYPERTWIST_REVERSE_SSH_LIVE_CONNECTION_FACTS_2026-06-04.md +++ b/docs/ops/HYPERTWIST_REVERSE_SSH_LIVE_CONNECTION_FACTS_2026-06-04.md @@ -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, diff --git a/docs/ops/HYPERTWIST_REVERSE_SSH_WINDOWS_BUILD_LANE_VERIFICATION_2026-06-01.md b/docs/ops/HYPERTWIST_REVERSE_SSH_WINDOWS_BUILD_LANE_VERIFICATION_2026-06-01.md index 77ddfdb..83c908c 100644 --- a/docs/ops/HYPERTWIST_REVERSE_SSH_WINDOWS_BUILD_LANE_VERIFICATION_2026-06-01.md +++ b/docs/ops/HYPERTWIST_REVERSE_SSH_WINDOWS_BUILD_LANE_VERIFICATION_2026-06-01.md @@ -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: diff --git a/docs/ops/HYPERTWIST_UNWIRED_REPO_INVENTORY_AND_WIRING_SCHEDULE_2026-06-10.md b/docs/ops/HYPERTWIST_UNWIRED_REPO_INVENTORY_AND_WIRING_SCHEDULE_2026-06-10.md index a05c301..55ec901 100644 --- a/docs/ops/HYPERTWIST_UNWIRED_REPO_INVENTORY_AND_WIRING_SCHEDULE_2026-06-10.md +++ b/docs/ops/HYPERTWIST_UNWIRED_REPO_INVENTORY_AND_WIRING_SCHEDULE_2026-06-10.md @@ -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. diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/API.md b/docs/v6_5_deep_manual_pack/HyperTwist/API.md index f432661..22f4c89 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/API.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/API.md @@ -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 diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md b/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md index ab38f17..a7eacb8 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md @@ -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 diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index 13cded7..d9b74cf 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -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. | diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md index f776b96..ff5fc49 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md @@ -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`