Implement HyperTwist Phase 6A Melinda runtime
This commit is contained in:
parent
69def4748a
commit
ff885162ce
21 changed files with 2698 additions and 13 deletions
|
|
@ -0,0 +1,509 @@
|
|||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
|
||||
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
|
||||
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "ProceduralMeshComponent/Public/ProceduralMeshComponent.h"
|
||||
|
||||
namespace HyperTwistMelindaProjectionActorInternal
|
||||
{
|
||||
constexpr float ClickTraceDistance = 20000.0f;
|
||||
|
||||
FHyperTwistPuzzleDefinitionRef ResolveDefinition(const FHyperTwistPuzzleState& State)
|
||||
{
|
||||
return State.Definition.IsStructurallyValid()
|
||||
? State.Definition
|
||||
: UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
AHyperTwistMelindaProjectionActor::AHyperTwistMelindaProjectionActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
|
||||
RootComponent = SceneRoot;
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::OnConstruction(const FTransform& Transform)
|
||||
{
|
||||
Super::OnConstruction(Transform);
|
||||
|
||||
if (!bGenerateOnConstruction)
|
||||
{
|
||||
if (CurrentState.IsStructurallyValid())
|
||||
{
|
||||
RefreshProjection();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bUsePreviewRandomState)
|
||||
{
|
||||
if (!GenerateRandomState(PreviewRandomSeed))
|
||||
{
|
||||
ResetToSolvedState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ResetToSolvedState();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (bGenerateOnConstruction && CurrentState.IsStructurallyValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (bUsePreviewRandomState)
|
||||
{
|
||||
if (!GenerateRandomState(PreviewRandomSeed))
|
||||
{
|
||||
ResetToSolvedState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ResetToSolvedState();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::ResetToSolvedState()
|
||||
{
|
||||
CurrentState = UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
|
||||
LastAppliedNotation = TEXT("solved");
|
||||
LastWarnings.Reset();
|
||||
RefreshProjection();
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionActor::GenerateRandomState(const int32 RandomSeed)
|
||||
{
|
||||
const FHyperTwistRandomStateGenerationResult GeneratedState =
|
||||
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(
|
||||
HyperTwistMelindaProjectionActorInternal::ResolveDefinition(CurrentState),
|
||||
RandomSeed
|
||||
);
|
||||
|
||||
LastWarnings = GeneratedState.Warnings;
|
||||
if (!GeneratedState.bGenerated || !GeneratedState.Validation.bIsSolvable)
|
||||
{
|
||||
if (!LastWarnings.Contains(TEXT("melinda-random-state-generation-failed")))
|
||||
{
|
||||
LastWarnings.Add(TEXT("melinda-random-state-generation-failed"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentState = GeneratedState.State;
|
||||
LastAppliedNotation = FString::Printf(TEXT("random/%d"), GeneratedState.SeedUsed);
|
||||
return RefreshProjection();
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionActor::ApplyTurnRequest(
|
||||
const FHyperTwistMelindaCellTurnRequest& Request
|
||||
)
|
||||
{
|
||||
const FHyperTwistMelindaCellTurnTransformBuildResult TransformResult =
|
||||
UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
|
||||
HyperTwistMelindaProjectionActorInternal::ResolveDefinition(CurrentState),
|
||||
Request
|
||||
);
|
||||
|
||||
LastWarnings = TransformResult.Warnings;
|
||||
if (!TransformResult.bBuilt || !TransformResult.bExactTransform)
|
||||
{
|
||||
if (!LastWarnings.Contains(TEXT("melinda-cell-turn-build-failed")))
|
||||
{
|
||||
LastWarnings.Add(TEXT("melinda-cell-turn-build-failed"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistApplyTransformationResult AppliedResult =
|
||||
UHyperTwistCoreLibrary::ApplyTransformation(CurrentState, TransformResult.Transformation);
|
||||
LastWarnings.Append(AppliedResult.Warnings);
|
||||
if (!AppliedResult.bApplied || !AppliedResult.bExactStateUpdate)
|
||||
{
|
||||
if (!LastWarnings.Contains(TEXT("melinda-cell-turn-apply-failed")))
|
||||
{
|
||||
LastWarnings.Add(TEXT("melinda-cell-turn-apply-failed"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentState = AppliedResult.State;
|
||||
LastAppliedNotation = AppliedResult.AppliedNotation;
|
||||
return RefreshProjection();
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionActor::ProcessClick(
|
||||
const FVector& RayOrigin,
|
||||
const FVector& RayDirection,
|
||||
const bool bCounterClockwise
|
||||
)
|
||||
{
|
||||
if (GetWorld() == nullptr || RayDirection.IsNearlyZero())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(MelindaProjectionClick), false);
|
||||
const FVector TraceEnd = RayOrigin + (RayDirection.GetSafeNormal() * HyperTwistMelindaProjectionActorInternal::ClickTraceDistance);
|
||||
if (!GetWorld()->LineTraceSingleByChannel(
|
||||
Hit,
|
||||
RayOrigin,
|
||||
TraceEnd,
|
||||
ECC_Visibility,
|
||||
QueryParams
|
||||
))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UProceduralMeshComponent* Mesh = Cast<UProceduralMeshComponent>(Hit.GetComponent());
|
||||
const FDisplayedCubieMetadata* Metadata = Mesh != nullptr ? DisplayedCubieMetadata.Find(Mesh) : nullptr;
|
||||
if (Metadata == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FVector LocalImpactNormal =
|
||||
Mesh->GetComponentTransform().InverseTransformVectorNoScale(Hit.ImpactNormal);
|
||||
int32 LocalAxisIndex = INDEX_NONE;
|
||||
if (!TryResolveLocalAxisFromImpactNormal(LocalImpactNormal, LocalAxisIndex)
|
||||
|| !Metadata->LocalAxes.IsValidIndex(LocalAxisIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FHyperTwistMelindaCellTurnRequest Request;
|
||||
Request.Cell = Metadata->Cell;
|
||||
Request.RotationAxis = Metadata->LocalAxes[LocalAxisIndex];
|
||||
Request.Direction = bCounterClockwise
|
||||
? EHyperTwistMelindaTurnDirection::CounterClockwise
|
||||
: EHyperTwistMelindaTurnDirection::Clockwise;
|
||||
return ApplyTurnRequest(Request);
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionActor::RefreshProjection()
|
||||
{
|
||||
const FHyperTwistMelindaCellFirstProjectionBuildResult ProjectionResult =
|
||||
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(CurrentState, CellCenterSpacing);
|
||||
|
||||
LastWarnings.Append(ProjectionResult.Warnings);
|
||||
if (!ProjectionResult.bProjected || !ProjectionResult.Projection.IsStructurallyValid())
|
||||
{
|
||||
CurrentProjection = FHyperTwistMelindaCellFirstProjection();
|
||||
ClearRenderedCells();
|
||||
if (!LastWarnings.Contains(TEXT("melinda-cell-first-projection-failed")))
|
||||
{
|
||||
LastWarnings.Add(TEXT("melinda-cell-first-projection-failed"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentProjection = ProjectionResult.Projection;
|
||||
BuildRenderedCells();
|
||||
return ProjectionResult.bExactProjection;
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::ClearRenderedCells()
|
||||
{
|
||||
DisplayedCubieMetadata.Empty();
|
||||
|
||||
for (UProceduralMeshComponent* Mesh : SpawnedCubieMeshes)
|
||||
{
|
||||
if (IsValid(Mesh))
|
||||
{
|
||||
RemoveInstanceComponent(Mesh);
|
||||
Mesh->DestroyComponent();
|
||||
}
|
||||
}
|
||||
SpawnedCubieMeshes.Reset();
|
||||
|
||||
for (USceneComponent* CellRoot : SpawnedCellRoots)
|
||||
{
|
||||
if (IsValid(CellRoot))
|
||||
{
|
||||
RemoveInstanceComponent(CellRoot);
|
||||
CellRoot->DestroyComponent();
|
||||
}
|
||||
}
|
||||
SpawnedCellRoots.Reset();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::BuildRenderedCells()
|
||||
{
|
||||
ClearRenderedCells();
|
||||
if (!CurrentProjection.IsStructurallyValid() || SceneRoot == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const FHyperTwistMelindaProjectedCell& CellProjection : CurrentProjection.Cells)
|
||||
{
|
||||
BuildProjectedCellRoot(CellProjection);
|
||||
}
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::BuildProjectedCellRoot(
|
||||
const FHyperTwistMelindaProjectedCell& CellProjection
|
||||
)
|
||||
{
|
||||
if (SceneRoot == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FName ComponentName = MakeUniqueObjectName(
|
||||
this,
|
||||
USceneComponent::StaticClass(),
|
||||
*FString::Printf(TEXT("MelindaCell_%s"), *CellProjection.CellLabel)
|
||||
);
|
||||
USceneComponent* CellRoot = NewObject<USceneComponent>(this, ComponentName, RF_Transactional);
|
||||
if (CellRoot == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddInstanceComponent(CellRoot);
|
||||
CellRoot->RegisterComponent();
|
||||
CellRoot->AttachToComponent(SceneRoot, FAttachmentTransformRules::KeepRelativeTransform);
|
||||
CellRoot->SetRelativeLocation(CellProjection.DisplayCenter);
|
||||
SpawnedCellRoots.Add(CellRoot);
|
||||
|
||||
for (const FHyperTwistMelindaProjectedCubie& CubieProjection : CellProjection.Cubies)
|
||||
{
|
||||
BuildProjectedCubie(CellRoot, CellProjection, CubieProjection);
|
||||
}
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::BuildProjectedCubie(
|
||||
USceneComponent* CellRoot,
|
||||
const FHyperTwistMelindaProjectedCell& CellProjection,
|
||||
const FHyperTwistMelindaProjectedCubie& CubieProjection
|
||||
)
|
||||
{
|
||||
if (CellRoot == nullptr || CubieProjection.Faces.Num() != 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FName ComponentName = MakeUniqueObjectName(
|
||||
this,
|
||||
UProceduralMeshComponent::StaticClass(),
|
||||
*FString::Printf(
|
||||
TEXT("MelindaCubie_%s_%02d"),
|
||||
*CellProjection.CellLabel,
|
||||
CubieProjection.PositionIndex
|
||||
)
|
||||
);
|
||||
UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(
|
||||
this,
|
||||
ComponentName,
|
||||
RF_Transactional
|
||||
);
|
||||
if (Mesh == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddInstanceComponent(Mesh);
|
||||
Mesh->RegisterComponent();
|
||||
Mesh->AttachToComponent(CellRoot, FAttachmentTransformRules::KeepRelativeTransform);
|
||||
Mesh->SetMobility(EComponentMobility::Movable);
|
||||
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
Mesh->SetCollisionObjectType(ECC_WorldDynamic);
|
||||
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
|
||||
Mesh->bUseComplexAsSimpleCollision = true;
|
||||
|
||||
const FVector Center = FVector(
|
||||
static_cast<float>(CubieProjection.LocalGridCoordinate.X),
|
||||
static_cast<float>(CubieProjection.LocalGridCoordinate.Y),
|
||||
static_cast<float>(CubieProjection.LocalGridCoordinate.Z)
|
||||
) * (CubieSize + CubieGap);
|
||||
const float HalfSize = CubieSize * 0.5f;
|
||||
|
||||
FDisplayedCubieMetadata Metadata;
|
||||
Metadata.Cell = CellProjection.Cell;
|
||||
Metadata.LocalAxes.Reserve(3);
|
||||
for (int32 FaceIndex = 0; FaceIndex < CubieProjection.Faces.Num(); ++FaceIndex)
|
||||
{
|
||||
const FHyperTwistMelindaProjectedCubieFace& FaceProjection =
|
||||
CubieProjection.Faces[FaceIndex];
|
||||
Metadata.LocalAxes.Add(FaceProjection.Axis);
|
||||
CreateCubieFace(
|
||||
Mesh,
|
||||
FaceIndex,
|
||||
Center,
|
||||
HalfSize,
|
||||
FaceIndex,
|
||||
FaceProjection.bPositiveSide,
|
||||
ResolveStickerColor(FaceProjection.Color)
|
||||
);
|
||||
}
|
||||
|
||||
DisplayedCubieMetadata.Add(Mesh, Metadata);
|
||||
SpawnedCubieMeshes.Add(Mesh);
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionActor::CreateCubieFace(
|
||||
UProceduralMeshComponent* Mesh,
|
||||
const int32 SectionIndex,
|
||||
const FVector& Center,
|
||||
const float HalfSize,
|
||||
const int32 LocalAxisIndex,
|
||||
const bool bPositiveSide,
|
||||
const FLinearColor& FaceColor
|
||||
)
|
||||
{
|
||||
if (Mesh == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FVector> Vertices;
|
||||
TArray<int32> Triangles;
|
||||
TArray<FVector> Normals;
|
||||
TArray<FVector2D> UVs;
|
||||
TArray<FLinearColor> VertexColors;
|
||||
TArray<FProcMeshTangent> Tangents;
|
||||
|
||||
FVector Normal = FVector::ZeroVector;
|
||||
FVector TangentX = FVector::ForwardVector;
|
||||
switch (LocalAxisIndex)
|
||||
{
|
||||
case 0:
|
||||
Normal = FVector(bPositiveSide ? 1.0f : -1.0f, 0.0f, 0.0f);
|
||||
TangentX = FVector(0.0f, 1.0f, 0.0f);
|
||||
Vertices = {
|
||||
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, -HalfSize, -HalfSize),
|
||||
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, +HalfSize, -HalfSize),
|
||||
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, +HalfSize, +HalfSize),
|
||||
Center + FVector(bPositiveSide ? HalfSize : -HalfSize, -HalfSize, +HalfSize)
|
||||
};
|
||||
if (!bPositiveSide)
|
||||
{
|
||||
Swap(Vertices[1], Vertices[3]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
Normal = FVector(0.0f, bPositiveSide ? 1.0f : -1.0f, 0.0f);
|
||||
TangentX = FVector(1.0f, 0.0f, 0.0f);
|
||||
Vertices = {
|
||||
Center + FVector(-HalfSize, bPositiveSide ? HalfSize : -HalfSize, -HalfSize),
|
||||
Center + FVector(+HalfSize, bPositiveSide ? HalfSize : -HalfSize, -HalfSize),
|
||||
Center + FVector(+HalfSize, bPositiveSide ? HalfSize : -HalfSize, +HalfSize),
|
||||
Center + FVector(-HalfSize, bPositiveSide ? HalfSize : -HalfSize, +HalfSize)
|
||||
};
|
||||
if (bPositiveSide)
|
||||
{
|
||||
Swap(Vertices[1], Vertices[3]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Normal = FVector(0.0f, 0.0f, bPositiveSide ? 1.0f : -1.0f);
|
||||
TangentX = FVector(1.0f, 0.0f, 0.0f);
|
||||
Vertices = {
|
||||
Center + FVector(-HalfSize, -HalfSize, bPositiveSide ? HalfSize : -HalfSize),
|
||||
Center + FVector(+HalfSize, -HalfSize, bPositiveSide ? HalfSize : -HalfSize),
|
||||
Center + FVector(+HalfSize, +HalfSize, bPositiveSide ? HalfSize : -HalfSize),
|
||||
Center + FVector(-HalfSize, +HalfSize, bPositiveSide ? HalfSize : -HalfSize)
|
||||
};
|
||||
if (!bPositiveSide)
|
||||
{
|
||||
Swap(Vertices[1], Vertices[3]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Triangles = {0, 1, 2, 0, 2, 3};
|
||||
Normals.Init(Normal, 4);
|
||||
UVs = {
|
||||
FVector2D(0.0f, 0.0f),
|
||||
FVector2D(1.0f, 0.0f),
|
||||
FVector2D(1.0f, 1.0f),
|
||||
FVector2D(0.0f, 1.0f)
|
||||
};
|
||||
VertexColors.Init(FaceColor, 4);
|
||||
Tangents.Init(FProcMeshTangent(TangentX, false), 4);
|
||||
|
||||
Mesh->CreateMeshSection_LinearColor(
|
||||
SectionIndex,
|
||||
Vertices,
|
||||
Triangles,
|
||||
Normals,
|
||||
UVs,
|
||||
VertexColors,
|
||||
Tangents,
|
||||
true
|
||||
);
|
||||
|
||||
if (VertexColorMaterial != nullptr)
|
||||
{
|
||||
Mesh->SetMaterial(SectionIndex, VertexColorMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionActor::TryResolveLocalAxisFromImpactNormal(
|
||||
const FVector& ImpactNormal,
|
||||
int32& OutLocalAxisIndex
|
||||
)
|
||||
{
|
||||
const FVector Normal = ImpactNormal.GetSafeNormal();
|
||||
if (Normal.IsNearlyZero())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FVector AbsNormal = Normal.GetAbs();
|
||||
if (AbsNormal.X >= AbsNormal.Y && AbsNormal.X >= AbsNormal.Z)
|
||||
{
|
||||
OutLocalAxisIndex = 0;
|
||||
}
|
||||
else if (AbsNormal.Y >= AbsNormal.Z)
|
||||
{
|
||||
OutLocalAxisIndex = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutLocalAxisIndex = 2;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FLinearColor AHyperTwistMelindaProjectionActor::ResolveStickerColor(
|
||||
const EHyperTwistMelindaStickerColor StickerColor
|
||||
)
|
||||
{
|
||||
switch (StickerColor)
|
||||
{
|
||||
case EHyperTwistMelindaStickerColor::XPositive:
|
||||
return FLinearColor(0.88f, 0.16f, 0.12f);
|
||||
case EHyperTwistMelindaStickerColor::XNegative:
|
||||
return FLinearColor(1.0f, 0.48f, 0.0f);
|
||||
case EHyperTwistMelindaStickerColor::YPositive:
|
||||
return FLinearColor(0.1f, 0.62f, 0.22f);
|
||||
case EHyperTwistMelindaStickerColor::YNegative:
|
||||
return FLinearColor(0.09f, 0.31f, 0.8f);
|
||||
case EHyperTwistMelindaStickerColor::ZPositive:
|
||||
return FLinearColor(0.95f, 0.95f, 0.95f);
|
||||
case EHyperTwistMelindaStickerColor::ZNegative:
|
||||
return FLinearColor(0.95f, 0.82f, 0.14f);
|
||||
case EHyperTwistMelindaStickerColor::WPositive:
|
||||
return FLinearColor(0.0f, 0.72f, 0.78f);
|
||||
case EHyperTwistMelindaStickerColor::WNegative:
|
||||
return FLinearColor(0.72f, 0.14f, 0.62f);
|
||||
default:
|
||||
return FLinearColor(0.12f, 0.12f, 0.12f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h"
|
||||
|
||||
AHyperTwistMelindaProjectionGameMode::AHyperTwistMelindaProjectionGameMode()
|
||||
{
|
||||
PlayerControllerClass = AHyperTwistMelindaProjectionPlayerController::StaticClass();
|
||||
DefaultPawnClass = AHyperTwistMelindaProjectionOrbitPawn::StaticClass();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (APlayerController* PlayerController =
|
||||
GetWorld() != nullptr ? GetWorld()->GetFirstPlayerController() : nullptr)
|
||||
{
|
||||
PlayerController->bShowMouseCursor = true;
|
||||
PlayerController->bEnableClickEvents = true;
|
||||
PlayerController->bEnableMouseOverEvents = true;
|
||||
}
|
||||
|
||||
AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor();
|
||||
if (ProjectionActor == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (bRandomizeOnBeginPlay)
|
||||
{
|
||||
ProjectionActor->GenerateRandomState(StartupRandomSeed);
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectionActor->ResetToSolvedState();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionGameMode::ResetProjectionToSolved()
|
||||
{
|
||||
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor())
|
||||
{
|
||||
ProjectionActor->ResetToSolvedState();
|
||||
}
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionGameMode::GenerateProjectionRandomState(const int32 RandomSeed)
|
||||
{
|
||||
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveOrSpawnProjectionActor())
|
||||
{
|
||||
return ProjectionActor->GenerateRandomState(RandomSeed);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
AHyperTwistMelindaProjectionActor* AHyperTwistMelindaProjectionGameMode::ResolveOrSpawnProjectionActor()
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (ActiveProjectionActor != nullptr)
|
||||
{
|
||||
return ActiveProjectionActor;
|
||||
}
|
||||
|
||||
for (TActorIterator<AHyperTwistMelindaProjectionActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
|
||||
{
|
||||
ActiveProjectionActor = *ActorIt;
|
||||
return ActiveProjectionActor;
|
||||
}
|
||||
|
||||
if (!bAutoSpawnProjectionActor)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSubclassOf<AHyperTwistMelindaProjectionActor> SpawnClass = ProjectionActorClass;
|
||||
if (SpawnClass == nullptr)
|
||||
{
|
||||
SpawnClass = AHyperTwistMelindaProjectionActor::StaticClass();
|
||||
}
|
||||
|
||||
FActorSpawnParameters SpawnParameters;
|
||||
SpawnParameters.SpawnCollisionHandlingOverride =
|
||||
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
ActiveProjectionActor = GetWorld()->SpawnActor<AHyperTwistMelindaProjectionActor>(
|
||||
SpawnClass,
|
||||
ProjectionSpawnLocation,
|
||||
ProjectionSpawnRotation,
|
||||
SpawnParameters
|
||||
);
|
||||
return ActiveProjectionActor;
|
||||
}
|
||||
|
|
@ -0,0 +1,652 @@
|
|||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h"
|
||||
|
||||
#include "JsonObjectConverter.h"
|
||||
|
||||
namespace HyperTwistMelindaProjectionLibraryInternal
|
||||
{
|
||||
struct FCellDescriptor
|
||||
{
|
||||
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
|
||||
EHyperTwistMelindaCellAxis FixedAxis = EHyperTwistMelindaCellAxis::W;
|
||||
bool bPositiveSide = true;
|
||||
const TCHAR* Label = TEXT("O");
|
||||
};
|
||||
|
||||
template <typename TStruct>
|
||||
bool DeserializePayload(const FHyperTwistSerializedPayload& Payload, TStruct& OutValue)
|
||||
{
|
||||
return !Payload.PayloadJson.IsEmpty()
|
||||
&& FJsonObjectConverter::JsonObjectStringToUStruct(Payload.PayloadJson, &OutValue, 0, 0);
|
||||
}
|
||||
|
||||
template <typename TStruct>
|
||||
FString SerializeStructToJson(const TStruct& Value)
|
||||
{
|
||||
FString Json;
|
||||
FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
|
||||
return Json;
|
||||
}
|
||||
|
||||
int32 ToAxisIndex(const EHyperTwistMelindaCellAxis Axis)
|
||||
{
|
||||
return static_cast<int32>(Axis);
|
||||
}
|
||||
|
||||
EHyperTwistMelindaCellAxis ToAxisEnum(const int32 AxisIndex)
|
||||
{
|
||||
switch (AxisIndex)
|
||||
{
|
||||
case 0:
|
||||
return EHyperTwistMelindaCellAxis::X;
|
||||
case 1:
|
||||
return EHyperTwistMelindaCellAxis::Y;
|
||||
case 2:
|
||||
return EHyperTwistMelindaCellAxis::Z;
|
||||
default:
|
||||
return EHyperTwistMelindaCellAxis::W;
|
||||
}
|
||||
}
|
||||
|
||||
FCellDescriptor GetCellDescriptor(const EHyperTwistMelindaCell Cell)
|
||||
{
|
||||
switch (Cell)
|
||||
{
|
||||
case EHyperTwistMelindaCell::Left:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::X, false, TEXT("L")};
|
||||
case EHyperTwistMelindaCell::Right:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::X, true, TEXT("R")};
|
||||
case EHyperTwistMelindaCell::Back:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::Y, false, TEXT("B")};
|
||||
case EHyperTwistMelindaCell::Front:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::Y, true, TEXT("F")};
|
||||
case EHyperTwistMelindaCell::Down:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::Z, false, TEXT("D")};
|
||||
case EHyperTwistMelindaCell::Up:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::Z, true, TEXT("U")};
|
||||
case EHyperTwistMelindaCell::Inner:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::W, false, TEXT("I")};
|
||||
default:
|
||||
return {Cell, EHyperTwistMelindaCellAxis::W, true, TEXT("O")};
|
||||
}
|
||||
}
|
||||
|
||||
int32 GetSignatureBit(const int32 SignatureId, const int32 AxisIndex)
|
||||
{
|
||||
return AxisIndex >= 0 && AxisIndex < 4 ? ((SignatureId >> AxisIndex) & 1) : 0;
|
||||
}
|
||||
|
||||
TArray<int32> GetRemainingAxes(const int32 FixedAxisIndex)
|
||||
{
|
||||
TArray<int32> RemainingAxes;
|
||||
RemainingAxes.Reserve(3);
|
||||
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
|
||||
{
|
||||
if (AxisIndex != FixedAxisIndex)
|
||||
{
|
||||
RemainingAxes.Add(AxisIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return RemainingAxes;
|
||||
}
|
||||
|
||||
TArray<int32> BuildIdentityPullMap()
|
||||
{
|
||||
TArray<int32> PullMap;
|
||||
PullMap.Reserve(16);
|
||||
for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
|
||||
{
|
||||
PullMap.Add(PositionIndex);
|
||||
}
|
||||
|
||||
return PullMap;
|
||||
}
|
||||
|
||||
TArray<int32> BuildIdentityOrientationDeltas()
|
||||
{
|
||||
TArray<int32> Deltas;
|
||||
Deltas.Init(0, 16);
|
||||
return Deltas;
|
||||
}
|
||||
|
||||
FHyperTwistPuzzleState BuildSolvedMelindaState(const FHyperTwistPuzzleDefinitionRef& Definition)
|
||||
{
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding StateEncoding;
|
||||
StateEncoding.PositionToPiece.Reserve(16);
|
||||
for (int32 PieceId = 0; PieceId < 16; ++PieceId)
|
||||
{
|
||||
StateEncoding.PositionToPiece.Add(PieceId);
|
||||
}
|
||||
|
||||
StateEncoding.PieceOrientation.Init(0, 16);
|
||||
|
||||
FHyperTwistPuzzleState PuzzleState;
|
||||
PuzzleState.Definition = Definition;
|
||||
PuzzleState.StateEncodingKind = EHyperTwistStateEncodingKind::FamilySpecific;
|
||||
PuzzleState.StateEncoding.EncodingProfile = StateEncoding.EncodingProfile;
|
||||
PuzzleState.StateEncoding.PayloadJson = SerializeStructToJson(StateEncoding);
|
||||
PuzzleState.OrientationFrame.Reference = StateEncoding.FrameProfile;
|
||||
PuzzleState.bIsSolved = true;
|
||||
PuzzleState.Source = EHyperTwistStateSource::Runtime;
|
||||
return PuzzleState;
|
||||
}
|
||||
|
||||
bool IsSupportedDefinition(const FHyperTwistPuzzleDefinitionRef& Definition)
|
||||
{
|
||||
return Definition.IsStructurallyValid()
|
||||
&& Definition.PuzzleId == TEXT("hypercube/2x2x2x2")
|
||||
&& Definition.PuzzleFamily == EHyperTwistPuzzleFamily::Hypercube
|
||||
&& Definition.Dimension == 4
|
||||
&& Definition.SizeVector == TArray<int32>({2, 2, 2, 2});
|
||||
}
|
||||
|
||||
bool TryDeserializeMelindaState(
|
||||
const FHyperTwistPuzzleState& State,
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding& OutState,
|
||||
TArray<FString>& OutWarnings
|
||||
)
|
||||
{
|
||||
if (State.StateEncoding.EncodingProfile != TEXT("melinda-2x2x2x2-state-v1")
|
||||
|| !IsSupportedDefinition(State.Definition))
|
||||
{
|
||||
OutWarnings.Add(TEXT("unsupported-melinda-state"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DeserializePayload(State.StateEncoding, OutState) || !OutState.IsStructurallyValid())
|
||||
{
|
||||
OutWarnings.Add(TEXT("invalid-melinda-state-payload"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const TArray<TArray<int32>>& GetOrientationPermutations()
|
||||
{
|
||||
static TArray<TArray<int32>> Permutations;
|
||||
if (Permutations.Num() == 0)
|
||||
{
|
||||
for (int32 A = 0; A < 4; ++A)
|
||||
{
|
||||
for (int32 B = 0; B < 4; ++B)
|
||||
{
|
||||
if (B == A)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int32 C = 0; C < 4; ++C)
|
||||
{
|
||||
if (C == A || C == B)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int32 D = 0; D < 4; ++D)
|
||||
{
|
||||
if (D == A || D == B || D == C)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Permutations.Add({A, B, C, D});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Permutations;
|
||||
}
|
||||
|
||||
EHyperTwistMelindaStickerColor GetStickerColor(const int32 PieceId, const int32 LocalAxisIndex)
|
||||
{
|
||||
if (LocalAxisIndex < 0 || LocalAxisIndex >= 4)
|
||||
{
|
||||
return EHyperTwistMelindaStickerColor::Unknown;
|
||||
}
|
||||
|
||||
const bool bPositiveColor = GetSignatureBit(PieceId, LocalAxisIndex) != 0;
|
||||
switch (LocalAxisIndex)
|
||||
{
|
||||
case 0:
|
||||
return bPositiveColor
|
||||
? EHyperTwistMelindaStickerColor::XPositive
|
||||
: EHyperTwistMelindaStickerColor::XNegative;
|
||||
case 1:
|
||||
return bPositiveColor
|
||||
? EHyperTwistMelindaStickerColor::YPositive
|
||||
: EHyperTwistMelindaStickerColor::YNegative;
|
||||
case 2:
|
||||
return bPositiveColor
|
||||
? EHyperTwistMelindaStickerColor::ZPositive
|
||||
: EHyperTwistMelindaStickerColor::ZNegative;
|
||||
default:
|
||||
return bPositiveColor
|
||||
? EHyperTwistMelindaStickerColor::WPositive
|
||||
: EHyperTwistMelindaStickerColor::WNegative;
|
||||
}
|
||||
}
|
||||
|
||||
FString GetCellLetterForAxisAndSign(const int32 AxisIndex, const bool bPositiveSide)
|
||||
{
|
||||
switch (AxisIndex)
|
||||
{
|
||||
case 0:
|
||||
return bPositiveSide ? TEXT("R") : TEXT("L");
|
||||
case 1:
|
||||
return bPositiveSide ? TEXT("F") : TEXT("B");
|
||||
case 2:
|
||||
return bPositiveSide ? TEXT("U") : TEXT("D");
|
||||
default:
|
||||
return bPositiveSide ? TEXT("O") : TEXT("I");
|
||||
}
|
||||
}
|
||||
|
||||
FString BuildTouchLabel(const int32 SignatureId, const int32 ExcludedAxisIndex)
|
||||
{
|
||||
static const int32 AxisPriority[] = {3, 2, 1, 0};
|
||||
|
||||
FString Label;
|
||||
for (const int32 AxisIndex : AxisPriority)
|
||||
{
|
||||
if (AxisIndex == ExcludedAxisIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Label += GetCellLetterForAxisAndSign(
|
||||
AxisIndex,
|
||||
GetSignatureBit(SignatureId, AxisIndex) != 0
|
||||
);
|
||||
}
|
||||
|
||||
return Label;
|
||||
}
|
||||
|
||||
FVector GetProjectionBasis(const int32 AxisIndex)
|
||||
{
|
||||
switch (AxisIndex)
|
||||
{
|
||||
case 0:
|
||||
return FVector(1.0f, 0.0f, 0.0f);
|
||||
case 1:
|
||||
return FVector(0.0f, 1.0f, 0.0f);
|
||||
case 2:
|
||||
return FVector(0.0f, 0.0f, 1.0f);
|
||||
default:
|
||||
return FVector(0.7f, -0.55f, 0.8f).GetSafeNormal();
|
||||
}
|
||||
}
|
||||
|
||||
int32 GetOrientationDeltaIndexForRotationAxis(const int32 RotationAxisIndex)
|
||||
{
|
||||
// These two odd, twist-preserving orientation deltas keep the move family
|
||||
// closed under repeated application while still distinguishing the two
|
||||
// principal axis pairings in the owned Phase 6A interaction surface.
|
||||
return (RotationAxisIndex == 0 || RotationAxisIndex == 1) ? 6 : 1;
|
||||
}
|
||||
|
||||
void DecodePositionSigns(const int32 PositionIndex, int32 OutSigns[4])
|
||||
{
|
||||
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
|
||||
{
|
||||
OutSigns[AxisIndex] = GetSignatureBit(PositionIndex, AxisIndex) != 0 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
int32 EncodePositionSigns(const int32 Signs[4])
|
||||
{
|
||||
int32 PositionIndex = 0;
|
||||
for (int32 AxisIndex = 0; AxisIndex < 4; ++AxisIndex)
|
||||
{
|
||||
if (Signs[AxisIndex] > 0)
|
||||
{
|
||||
PositionIndex |= (1 << AxisIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return PositionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
FString UHyperTwistMelindaProjectionLibrary::GetCellLabel(const EHyperTwistMelindaCell Cell)
|
||||
{
|
||||
return HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).Label;
|
||||
}
|
||||
|
||||
FString UHyperTwistMelindaProjectionLibrary::GetAxisLabel(const EHyperTwistMelindaCellAxis Axis)
|
||||
{
|
||||
switch (Axis)
|
||||
{
|
||||
case EHyperTwistMelindaCellAxis::X:
|
||||
return TEXT("X");
|
||||
case EHyperTwistMelindaCellAxis::Y:
|
||||
return TEXT("Y");
|
||||
case EHyperTwistMelindaCellAxis::Z:
|
||||
return TEXT("Z");
|
||||
default:
|
||||
return TEXT("W");
|
||||
}
|
||||
}
|
||||
|
||||
bool UHyperTwistMelindaProjectionLibrary::IsAxisAvailableForCell(
|
||||
const EHyperTwistMelindaCell Cell,
|
||||
const EHyperTwistMelindaCellAxis Axis
|
||||
)
|
||||
{
|
||||
return HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).FixedAxis != Axis;
|
||||
}
|
||||
|
||||
TArray<EHyperTwistMelindaCellAxis> UHyperTwistMelindaProjectionLibrary::GetAvailableAxesForCell(
|
||||
const EHyperTwistMelindaCell Cell
|
||||
)
|
||||
{
|
||||
const int32 FixedAxisIndex = HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell).FixedAxis
|
||||
);
|
||||
|
||||
TArray<EHyperTwistMelindaCellAxis> Result;
|
||||
Result.Reserve(3);
|
||||
for (const int32 AxisIndex : HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex))
|
||||
{
|
||||
Result.Add(HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex));
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
FHyperTwistMelindaCellTurnTransformBuildResult UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
|
||||
const FHyperTwistPuzzleDefinitionRef& Definition,
|
||||
const FHyperTwistMelindaCellTurnRequest& Request
|
||||
)
|
||||
{
|
||||
FHyperTwistMelindaCellTurnTransformBuildResult Result;
|
||||
if (!HyperTwistMelindaProjectionLibraryInternal::IsSupportedDefinition(Definition))
|
||||
{
|
||||
Result.Warnings.Add(TEXT("unsupported-melinda-definition"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
const HyperTwistMelindaProjectionLibraryInternal::FCellDescriptor CellDescriptor =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Request.Cell);
|
||||
const int32 FixedAxisIndex =
|
||||
HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(CellDescriptor.FixedAxis);
|
||||
const int32 RotationAxisIndex =
|
||||
HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(Request.RotationAxis);
|
||||
if (FixedAxisIndex == RotationAxisIndex)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("rotation-axis-matches-fixed-cell-axis"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
const TArray<int32> RemainingAxes =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex);
|
||||
int32 PlaneAxisA = INDEX_NONE;
|
||||
int32 PlaneAxisB = INDEX_NONE;
|
||||
for (const int32 AxisIndex : RemainingAxes)
|
||||
{
|
||||
if (AxisIndex == RotationAxisIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PlaneAxisA == INDEX_NONE)
|
||||
{
|
||||
PlaneAxisA = AxisIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlaneAxisB = AxisIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (PlaneAxisA == INDEX_NONE || PlaneAxisB == INDEX_NONE)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("unable-to-resolve-turn-plane"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
FHyperTwistMelinda2x2x2x2TransformEncoding TransformEncoding;
|
||||
TransformEncoding.MoveId = FString::Printf(
|
||||
TEXT("melinda-cell-turn.%s.%s.%s"),
|
||||
*GetCellLabel(Request.Cell).ToLower(),
|
||||
*GetAxisLabel(Request.RotationAxis).ToLower(),
|
||||
Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise ? TEXT("cw") : TEXT("ccw")
|
||||
);
|
||||
TransformEncoding.PositionPullMap =
|
||||
HyperTwistMelindaProjectionLibraryInternal::BuildIdentityPullMap();
|
||||
TransformEncoding.OrientationDeltaPerNewPosition =
|
||||
HyperTwistMelindaProjectionLibraryInternal::BuildIdentityOrientationDeltas();
|
||||
|
||||
const int32 OrientationDeltaIndex =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetOrientationDeltaIndexForRotationAxis(
|
||||
RotationAxisIndex
|
||||
);
|
||||
|
||||
const int32 FixedAxisSign = CellDescriptor.bPositiveSide ? 1 : -1;
|
||||
for (int32 NewPosition = 0; NewPosition < 16; ++NewPosition)
|
||||
{
|
||||
int32 NewSigns[4];
|
||||
HyperTwistMelindaProjectionLibraryInternal::DecodePositionSigns(NewPosition, NewSigns);
|
||||
if (NewSigns[FixedAxisIndex] != FixedAxisSign)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int32 OldSigns[4] = {NewSigns[0], NewSigns[1], NewSigns[2], NewSigns[3]};
|
||||
if (Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise)
|
||||
{
|
||||
OldSigns[PlaneAxisA] = -NewSigns[PlaneAxisB];
|
||||
OldSigns[PlaneAxisB] = NewSigns[PlaneAxisA];
|
||||
}
|
||||
else
|
||||
{
|
||||
OldSigns[PlaneAxisA] = NewSigns[PlaneAxisB];
|
||||
OldSigns[PlaneAxisB] = -NewSigns[PlaneAxisA];
|
||||
}
|
||||
|
||||
TransformEncoding.PositionPullMap[NewPosition] =
|
||||
HyperTwistMelindaProjectionLibraryInternal::EncodePositionSigns(OldSigns);
|
||||
TransformEncoding.OrientationDeltaPerNewPosition[NewPosition] =
|
||||
OrientationDeltaIndex;
|
||||
}
|
||||
|
||||
Result.Transformation.Definition = Definition;
|
||||
Result.Transformation.TransformKind = EHyperTwistTransformKind::SingleMove;
|
||||
Result.Transformation.Notation = FString::Printf(
|
||||
TEXT("%s%s%s"),
|
||||
*GetCellLabel(Request.Cell),
|
||||
*GetAxisLabel(Request.RotationAxis),
|
||||
Request.Direction == EHyperTwistMelindaTurnDirection::Clockwise ? TEXT("") : TEXT("'")
|
||||
);
|
||||
Result.Transformation.TransformEncoding.EncodingProfile = TransformEncoding.EncodingProfile;
|
||||
Result.Transformation.TransformEncoding.PayloadJson =
|
||||
HyperTwistMelindaProjectionLibraryInternal::SerializeStructToJson(TransformEncoding);
|
||||
Result.Transformation.bInvertible = true;
|
||||
Result.Transformation.OriginalNotation = Result.Transformation.Notation;
|
||||
|
||||
if (!Result.Transformation.IsStructurallyValid())
|
||||
{
|
||||
Result.Warnings.Add(TEXT("invalid-melinda-cell-turn-transform"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
const FHyperTwistPuzzleState SolvedState =
|
||||
HyperTwistMelindaProjectionLibraryInternal::BuildSolvedMelindaState(Definition);
|
||||
const FHyperTwistApplyTransformationResult ApplyResult =
|
||||
UHyperTwistCoreLibrary::ApplyTransformation(SolvedState, Result.Transformation);
|
||||
Result.Warnings.Append(ApplyResult.Warnings);
|
||||
Result.bBuilt = ApplyResult.bApplied && ApplyResult.bExactStateUpdate;
|
||||
if (!Result.bBuilt)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("melinda-cell-turn-application-failed"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
const FHyperTwistPuzzleStateValidationResult Validation =
|
||||
UHyperTwistCoreLibrary::ValidatePuzzleState(ApplyResult.State);
|
||||
Result.bExactTransform = Validation.bIsSolvable;
|
||||
if (!Result.bExactTransform)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("melinda-cell-turn-result-not-solvable"));
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
FHyperTwistMelindaCellFirstProjectionBuildResult UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
|
||||
const FHyperTwistPuzzleState& State,
|
||||
const float CellCenterSpacing
|
||||
)
|
||||
{
|
||||
FHyperTwistMelindaCellFirstProjectionBuildResult Result;
|
||||
Result.SourceStateValidation = UHyperTwistCoreLibrary::ValidatePuzzleState(State);
|
||||
|
||||
FHyperTwistMelinda2x2x2x2StateEncoding MelindaState;
|
||||
if (!HyperTwistMelindaProjectionLibraryInternal::TryDeserializeMelindaState(
|
||||
State,
|
||||
MelindaState,
|
||||
Result.Warnings
|
||||
))
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
|
||||
if (!Result.SourceStateValidation.bStateSupported || !Result.SourceStateValidation.bStructureValid)
|
||||
{
|
||||
Result.Warnings.Add(TEXT("melinda-state-not-projectable"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
const TArray<TArray<int32>>& OrientationPermutations =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetOrientationPermutations();
|
||||
|
||||
Result.Projection.Definition = State.Definition;
|
||||
Result.Projection.Cells.Reserve(8);
|
||||
|
||||
static const EHyperTwistMelindaCell CellsInProjectionOrder[] = {
|
||||
EHyperTwistMelindaCell::Left,
|
||||
EHyperTwistMelindaCell::Right,
|
||||
EHyperTwistMelindaCell::Back,
|
||||
EHyperTwistMelindaCell::Front,
|
||||
EHyperTwistMelindaCell::Down,
|
||||
EHyperTwistMelindaCell::Up,
|
||||
EHyperTwistMelindaCell::Inner,
|
||||
EHyperTwistMelindaCell::Outer
|
||||
};
|
||||
|
||||
for (const EHyperTwistMelindaCell Cell : CellsInProjectionOrder)
|
||||
{
|
||||
const HyperTwistMelindaProjectionLibraryInternal::FCellDescriptor CellDescriptor =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetCellDescriptor(Cell);
|
||||
const int32 FixedAxisIndex =
|
||||
HyperTwistMelindaProjectionLibraryInternal::ToAxisIndex(CellDescriptor.FixedAxis);
|
||||
const TArray<int32> RemainingAxes =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetRemainingAxes(FixedAxisIndex);
|
||||
|
||||
FHyperTwistMelindaProjectedCell ProjectedCell;
|
||||
ProjectedCell.Cell = Cell;
|
||||
ProjectedCell.FixedAxis = CellDescriptor.FixedAxis;
|
||||
ProjectedCell.bPositiveSide = CellDescriptor.bPositiveSide;
|
||||
ProjectedCell.CellLabel = CellDescriptor.Label;
|
||||
ProjectedCell.DisplayCenter =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetProjectionBasis(FixedAxisIndex)
|
||||
* (CellDescriptor.bPositiveSide ? CellCenterSpacing : -CellCenterSpacing);
|
||||
ProjectedCell.Cubies.Reserve(8);
|
||||
for (const int32 AxisIndex : RemainingAxes)
|
||||
{
|
||||
ProjectedCell.AvailableAxes.Add(
|
||||
HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex)
|
||||
);
|
||||
}
|
||||
|
||||
for (int32 PositionIndex = 0; PositionIndex < MelindaState.PositionToPiece.Num(); ++PositionIndex)
|
||||
{
|
||||
if (HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(PositionIndex, FixedAxisIndex)
|
||||
!= (CellDescriptor.bPositiveSide ? 1 : 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32 PieceId = MelindaState.PositionToPiece[PositionIndex];
|
||||
if (!MelindaState.PieceOrientation.IsValidIndex(PieceId)
|
||||
|| !OrientationPermutations.IsValidIndex(MelindaState.PieceOrientation[PieceId]))
|
||||
{
|
||||
Result.Warnings.Add(TEXT("invalid-melinda-piece-orientation"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
const TArray<int32>& OrientationPermutation =
|
||||
OrientationPermutations[MelindaState.PieceOrientation[PieceId]];
|
||||
|
||||
FHyperTwistMelindaProjectedCubie ProjectedCubie;
|
||||
ProjectedCubie.PositionIndex = PositionIndex;
|
||||
ProjectedCubie.PieceId = PieceId;
|
||||
ProjectedCubie.LocalGridCoordinate = FIntVector(
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
|
||||
PositionIndex,
|
||||
RemainingAxes[0]
|
||||
) != 0 ? 1 : -1,
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
|
||||
PositionIndex,
|
||||
RemainingAxes[1]
|
||||
) != 0 ? 1 : -1,
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
|
||||
PositionIndex,
|
||||
RemainingAxes[2]
|
||||
) != 0 ? 1 : -1
|
||||
);
|
||||
ProjectedCubie.SlotLabel =
|
||||
HyperTwistMelindaProjectionLibraryInternal::BuildTouchLabel(
|
||||
PositionIndex,
|
||||
FixedAxisIndex
|
||||
);
|
||||
ProjectedCubie.PieceLabel =
|
||||
HyperTwistMelindaProjectionLibraryInternal::BuildTouchLabel(
|
||||
PieceId,
|
||||
FixedAxisIndex
|
||||
);
|
||||
ProjectedCubie.bPieceInSolvedPosition = PositionIndex == PieceId;
|
||||
ProjectedCubie.Faces.Reserve(3);
|
||||
|
||||
for (const int32 AxisIndex : RemainingAxes)
|
||||
{
|
||||
FHyperTwistMelindaProjectedCubieFace Face;
|
||||
Face.Axis = HyperTwistMelindaProjectionLibraryInternal::ToAxisEnum(AxisIndex);
|
||||
Face.bPositiveSide =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetSignatureBit(
|
||||
PositionIndex,
|
||||
AxisIndex
|
||||
) != 0;
|
||||
Face.Color =
|
||||
HyperTwistMelindaProjectionLibraryInternal::GetStickerColor(
|
||||
PieceId,
|
||||
OrientationPermutation[AxisIndex]
|
||||
);
|
||||
ProjectedCubie.Faces.Add(Face);
|
||||
}
|
||||
|
||||
ProjectedCell.Cubies.Add(ProjectedCubie);
|
||||
}
|
||||
|
||||
if (!ProjectedCell.IsStructurallyValid())
|
||||
{
|
||||
Result.Warnings.Add(TEXT("invalid-melinda-projected-cell"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
Result.Projection.Cells.Add(ProjectedCell);
|
||||
}
|
||||
|
||||
if (!Result.Projection.IsStructurallyValid())
|
||||
{
|
||||
Result.Warnings.Add(TEXT("invalid-melinda-cell-first-projection"));
|
||||
return Result;
|
||||
}
|
||||
|
||||
Result.bProjected = true;
|
||||
Result.bExactProjection = true;
|
||||
return Result;
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionOrbitPawn.h"
|
||||
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
|
||||
#include "InputCoreTypes.h"
|
||||
|
||||
AHyperTwistMelindaProjectionOrbitPawn::AHyperTwistMelindaProjectionOrbitPawn()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
|
||||
RootComponent = SceneRoot;
|
||||
|
||||
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
|
||||
SpringArm->SetupAttachment(SceneRoot);
|
||||
SpringArm->bDoCollisionTest = false;
|
||||
SpringArm->bEnableCameraLag = false;
|
||||
SpringArm->bUsePawnControlRotation = false;
|
||||
SpringArm->TargetArmLength = InitialArmLength;
|
||||
|
||||
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
|
||||
CameraComponent->SetupAttachment(SpringArm, USpringArmComponent::SocketName);
|
||||
CameraComponent->bUsePawnControlRotation = false;
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionOrbitPawn::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
CurrentYawDegrees = InitialYawDegrees;
|
||||
CurrentPitchDegrees = InitialPitchDegrees;
|
||||
RefreshOrbitFocusPointFromProjection();
|
||||
if (SpringArm != nullptr)
|
||||
{
|
||||
SpringArm->TargetArmLength = FMath::Clamp(
|
||||
InitialArmLength,
|
||||
MinimumArmLength,
|
||||
MaximumArmLength
|
||||
);
|
||||
}
|
||||
ApplyOrbitTransform();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionOrbitPawn::Tick(const float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
static_cast<void>(DeltaSeconds);
|
||||
|
||||
const FVector PreviousFocusPoint = OrbitFocusPoint;
|
||||
RefreshOrbitFocusPointFromProjection();
|
||||
if (!OrbitFocusPoint.Equals(PreviousFocusPoint))
|
||||
{
|
||||
ApplyOrbitTransform();
|
||||
}
|
||||
|
||||
APlayerController* PlayerController = Cast<APlayerController>(GetController());
|
||||
if (PlayerController == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SpringArm != nullptr)
|
||||
{
|
||||
const float MouseWheelDelta = PlayerController->GetInputAnalogKeyState(EKeys::MouseWheelAxis);
|
||||
if (!FMath::IsNearlyZero(MouseWheelDelta))
|
||||
{
|
||||
SpringArm->TargetArmLength = FMath::Clamp(
|
||||
SpringArm->TargetArmLength - (MouseWheelDelta * ZoomStep),
|
||||
MinimumArmLength,
|
||||
MaximumArmLength
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ShouldOrbitFromMouseInput())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float MouseDeltaX = 0.0f;
|
||||
float MouseDeltaY = 0.0f;
|
||||
PlayerController->GetInputMouseDelta(MouseDeltaX, MouseDeltaY);
|
||||
if (!FMath::IsNearlyZero(MouseDeltaX) || !FMath::IsNearlyZero(MouseDeltaY))
|
||||
{
|
||||
CurrentYawDegrees += MouseDeltaX * OrbitYawDegreesPerPixel;
|
||||
CurrentPitchDegrees = FMath::Clamp(
|
||||
CurrentPitchDegrees - (MouseDeltaY * OrbitPitchDegreesPerPixel),
|
||||
MinimumPitchDegrees,
|
||||
MaximumPitchDegrees
|
||||
);
|
||||
ApplyOrbitTransform();
|
||||
}
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionOrbitPawn::ApplyOrbitTransform()
|
||||
{
|
||||
SetActorLocation(OrbitFocusPoint);
|
||||
if (SpringArm != nullptr)
|
||||
{
|
||||
SpringArm->SetRelativeRotation(FRotator(CurrentPitchDegrees, CurrentYawDegrees, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionOrbitPawn::RefreshOrbitFocusPointFromProjection()
|
||||
{
|
||||
if (!bFollowActiveProjectionActor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (const AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
|
||||
{
|
||||
OrbitFocusPoint = ProjectionActor->GetActorLocation();
|
||||
}
|
||||
}
|
||||
|
||||
AHyperTwistMelindaProjectionActor* AHyperTwistMelindaProjectionOrbitPawn::ResolveProjectionActor() const
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (const AHyperTwistMelindaProjectionGameMode* GameMode =
|
||||
Cast<AHyperTwistMelindaProjectionGameMode>(GetWorld()->GetAuthGameMode()))
|
||||
{
|
||||
if (GameMode->ActiveProjectionActor != nullptr)
|
||||
{
|
||||
return GameMode->ActiveProjectionActor;
|
||||
}
|
||||
}
|
||||
|
||||
for (TActorIterator<AHyperTwistMelindaProjectionActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
|
||||
{
|
||||
return *ActorIt;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionOrbitPawn::ShouldOrbitFromMouseInput() const
|
||||
{
|
||||
const APlayerController* PlayerController = Cast<APlayerController>(GetController());
|
||||
if (PlayerController == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bMiddleMouseOrbit =
|
||||
bUseMiddleMouseOrbit && PlayerController->IsInputKeyDown(EKeys::MiddleMouseButton);
|
||||
const bool bShiftRightMouseOrbit =
|
||||
bUseRightMouseOrbitWithShift
|
||||
&& PlayerController->IsInputKeyDown(EKeys::RightMouseButton)
|
||||
&& (
|
||||
PlayerController->IsInputKeyDown(EKeys::LeftShift)
|
||||
|| PlayerController->IsInputKeyDown(EKeys::RightShift)
|
||||
);
|
||||
|
||||
return bMiddleMouseOrbit || bShiftRightMouseOrbit;
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionPlayerController.h"
|
||||
|
||||
#include "EngineUtils.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionActor.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionGameMode.h"
|
||||
#include "InputCoreTypes.h"
|
||||
|
||||
AHyperTwistMelindaProjectionPlayerController::AHyperTwistMelindaProjectionPlayerController()
|
||||
{
|
||||
bShowMouseCursor = true;
|
||||
bEnableClickEvents = true;
|
||||
bEnableMouseOverEvents = true;
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
ApplyInputMode();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
if (InputComponent == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InputComponent->BindKey(
|
||||
EKeys::LeftMouseButton,
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistMelindaProjectionPlayerController::HandlePrimaryClick
|
||||
);
|
||||
|
||||
if (bEnableCounterClockwiseRightClick)
|
||||
{
|
||||
InputComponent->BindKey(
|
||||
EKeys::RightMouseButton,
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistMelindaProjectionPlayerController::HandleSecondaryClick
|
||||
);
|
||||
}
|
||||
|
||||
if (bEnableTouchTurnInput)
|
||||
{
|
||||
InputComponent->BindTouch(
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistMelindaProjectionPlayerController::HandleTouchPressed
|
||||
);
|
||||
}
|
||||
|
||||
if (bBindResetShortcut)
|
||||
{
|
||||
InputComponent->BindKey(
|
||||
EKeys::R,
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistMelindaProjectionPlayerController::HandleResetShortcut
|
||||
);
|
||||
}
|
||||
|
||||
if (bBindRandomizeShortcut)
|
||||
{
|
||||
InputComponent->BindKey(
|
||||
EKeys::G,
|
||||
IE_Pressed,
|
||||
this,
|
||||
&AHyperTwistMelindaProjectionPlayerController::HandleRandomizeShortcut
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionPlayerController::TryProcessProjectionClickFromCursor(
|
||||
const bool bCounterClockwise
|
||||
)
|
||||
{
|
||||
float ScreenX = 0.0f;
|
||||
float ScreenY = 0.0f;
|
||||
if (!GetMousePosition(ScreenX, ScreenY))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryProcessProjectionClickFromScreenPosition(
|
||||
FVector2D(ScreenX, ScreenY),
|
||||
bCounterClockwise
|
||||
);
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionPlayerController::TryProcessProjectionClickFromScreenPosition(
|
||||
const FVector2D& ScreenPosition,
|
||||
const bool bCounterClockwise
|
||||
)
|
||||
{
|
||||
FVector RayOrigin = FVector::ZeroVector;
|
||||
FVector RayDirection = FVector::ZeroVector;
|
||||
if (!DeprojectScreenPositionToWorld(ScreenPosition.X, ScreenPosition.Y, RayOrigin, RayDirection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
|
||||
{
|
||||
return ProjectionActor->ProcessClick(RayOrigin, RayDirection, bCounterClockwise);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::ResetProjectionToSolved()
|
||||
{
|
||||
if (AHyperTwistMelindaProjectionGameMode* GameMode = ResolveProjectionGameMode())
|
||||
{
|
||||
GameMode->ResetProjectionToSolved();
|
||||
return;
|
||||
}
|
||||
|
||||
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
|
||||
{
|
||||
ProjectionActor->ResetToSolvedState();
|
||||
}
|
||||
}
|
||||
|
||||
bool AHyperTwistMelindaProjectionPlayerController::GenerateProjectionRandomState()
|
||||
{
|
||||
if (AHyperTwistMelindaProjectionGameMode* GameMode = ResolveProjectionGameMode())
|
||||
{
|
||||
return GameMode->GenerateProjectionRandomState(NextRandomSeed++);
|
||||
}
|
||||
|
||||
if (AHyperTwistMelindaProjectionActor* ProjectionActor = ResolveProjectionActor())
|
||||
{
|
||||
return ProjectionActor->GenerateRandomState(NextRandomSeed++);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::ApplyInputMode()
|
||||
{
|
||||
bShowMouseCursor = true;
|
||||
bEnableClickEvents = true;
|
||||
bEnableMouseOverEvents = true;
|
||||
|
||||
if (bUseGameAndUiInputMode)
|
||||
{
|
||||
FInputModeGameAndUI InputMode;
|
||||
InputMode.SetHideCursorDuringCapture(false);
|
||||
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
|
||||
SetInputMode(InputMode);
|
||||
}
|
||||
}
|
||||
|
||||
AHyperTwistMelindaProjectionActor* AHyperTwistMelindaProjectionPlayerController::ResolveProjectionActor() const
|
||||
{
|
||||
if (GetWorld() == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (const AHyperTwistMelindaProjectionGameMode* GameMode = ResolveProjectionGameMode())
|
||||
{
|
||||
if (GameMode->ActiveProjectionActor != nullptr)
|
||||
{
|
||||
return GameMode->ActiveProjectionActor;
|
||||
}
|
||||
}
|
||||
|
||||
for (TActorIterator<AHyperTwistMelindaProjectionActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
|
||||
{
|
||||
return *ActorIt;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AHyperTwistMelindaProjectionGameMode* AHyperTwistMelindaProjectionPlayerController::ResolveProjectionGameMode() const
|
||||
{
|
||||
return Cast<AHyperTwistMelindaProjectionGameMode>(
|
||||
GetWorld() != nullptr ? GetWorld()->GetAuthGameMode() : nullptr
|
||||
);
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::HandlePrimaryClick()
|
||||
{
|
||||
TryProcessProjectionClickFromCursor(false);
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::HandleSecondaryClick()
|
||||
{
|
||||
if (IsInputKeyDown(EKeys::LeftShift) || IsInputKeyDown(EKeys::RightShift))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryProcessProjectionClickFromCursor(true);
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::HandleResetShortcut()
|
||||
{
|
||||
ResetProjectionToSolved();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::HandleRandomizeShortcut()
|
||||
{
|
||||
GenerateProjectionRandomState();
|
||||
}
|
||||
|
||||
void AHyperTwistMelindaProjectionPlayerController::HandleTouchPressed(
|
||||
const ETouchIndex::Type FingerIndex,
|
||||
const FVector Location
|
||||
)
|
||||
{
|
||||
static_cast<void>(FingerIndex);
|
||||
TryProcessProjectionClickFromScreenPosition(FVector2D(Location.X, Location.Y), false);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h"
|
||||
#include "HyperTwistMelindaProjectionActor.generated.h"
|
||||
|
||||
class UProceduralMeshComponent;
|
||||
class USceneComponent;
|
||||
class UMaterialInterface;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AHyperTwistMelindaProjectionActor();
|
||||
|
||||
virtual void OnConstruction(const FTransform& Transform) override;
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
|
||||
float CellCenterSpacing = 120.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
|
||||
float CubieSize = 14.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
|
||||
float CubieGap = 1.75f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
|
||||
bool bGenerateOnConstruction = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
|
||||
bool bUsePreviewRandomState = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
|
||||
int32 PreviewRandomSeed = 2026;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
|
||||
TObjectPtr<USceneComponent> SceneRoot = nullptr;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Rendering")
|
||||
TObjectPtr<UMaterialInterface> VertexColorMaterial = nullptr;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
|
||||
FHyperTwistPuzzleState CurrentState;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
|
||||
FHyperTwistMelindaCellFirstProjection CurrentProjection;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
|
||||
FString LastAppliedNotation;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
|
||||
TArray<FString> LastWarnings;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
|
||||
void ResetToSolvedState();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
|
||||
bool GenerateRandomState(int32 RandomSeed);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
|
||||
bool ApplyTurnRequest(const FHyperTwistMelindaCellTurnRequest& Request);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
|
||||
bool ProcessClick(
|
||||
const FVector& RayOrigin,
|
||||
const FVector& RayDirection,
|
||||
bool bCounterClockwise = false
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
|
||||
bool RefreshProjection();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
|
||||
bool HasValidProjection() const
|
||||
{
|
||||
return CurrentProjection.IsStructurallyValid();
|
||||
}
|
||||
|
||||
protected:
|
||||
struct FDisplayedCubieMetadata
|
||||
{
|
||||
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
|
||||
TArray<EHyperTwistMelindaCellAxis> LocalAxes;
|
||||
};
|
||||
|
||||
void ClearRenderedCells();
|
||||
void BuildRenderedCells();
|
||||
void BuildProjectedCellRoot(const FHyperTwistMelindaProjectedCell& CellProjection);
|
||||
void BuildProjectedCubie(
|
||||
USceneComponent* CellRoot,
|
||||
const FHyperTwistMelindaProjectedCell& CellProjection,
|
||||
const FHyperTwistMelindaProjectedCubie& CubieProjection
|
||||
);
|
||||
void CreateCubieFace(
|
||||
UProceduralMeshComponent* Mesh,
|
||||
int32 SectionIndex,
|
||||
const FVector& Center,
|
||||
float HalfSize,
|
||||
int32 LocalAxisIndex,
|
||||
bool bPositiveSide,
|
||||
const FLinearColor& FaceColor
|
||||
);
|
||||
static bool TryResolveLocalAxisFromImpactNormal(
|
||||
const FVector& ImpactNormal,
|
||||
int32& OutLocalAxisIndex
|
||||
);
|
||||
static FLinearColor ResolveStickerColor(EHyperTwistMelindaStickerColor StickerColor);
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<USceneComponent>> SpawnedCellRoots;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UProceduralMeshComponent>> SpawnedCubieMeshes;
|
||||
|
||||
TMap<UProceduralMeshComponent*, FDisplayedCubieMetadata> DisplayedCubieMetadata;
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "HyperTwistMelindaProjectionGameMode.generated.h"
|
||||
|
||||
class AHyperTwistMelindaProjectionActor;
|
||||
class AHyperTwistMelindaProjectionOrbitPawn;
|
||||
class AHyperTwistMelindaProjectionPlayerController;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AHyperTwistMelindaProjectionGameMode();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bAutoSpawnProjectionActor = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
TSubclassOf<AHyperTwistMelindaProjectionActor> ProjectionActorClass;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FVector ProjectionSpawnLocation = FVector(0.0f, 0.0f, 160.0f);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FRotator ProjectionSpawnRotation = FRotator::ZeroRotator;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bRandomizeOnBeginPlay = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
int32 StartupRandomSeed = 2026;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda")
|
||||
TObjectPtr<AHyperTwistMelindaProjectionActor> ActiveProjectionActor = nullptr;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
|
||||
void ResetProjectionToSolved();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Melinda")
|
||||
bool GenerateProjectionRandomState(int32 RandomSeed);
|
||||
|
||||
protected:
|
||||
AHyperTwistMelindaProjectionActor* ResolveOrSpawnProjectionActor();
|
||||
};
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
|
||||
#include "HyperTwistMelindaProjectionLibrary.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistMelindaCell : uint8
|
||||
{
|
||||
Left UMETA(DisplayName = "Left"),
|
||||
Right UMETA(DisplayName = "Right"),
|
||||
Back UMETA(DisplayName = "Back"),
|
||||
Front UMETA(DisplayName = "Front"),
|
||||
Down UMETA(DisplayName = "Down"),
|
||||
Up UMETA(DisplayName = "Up"),
|
||||
Inner UMETA(DisplayName = "Inner"),
|
||||
Outer UMETA(DisplayName = "Outer")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistMelindaCellAxis : uint8
|
||||
{
|
||||
X UMETA(DisplayName = "X"),
|
||||
Y UMETA(DisplayName = "Y"),
|
||||
Z UMETA(DisplayName = "Z"),
|
||||
W UMETA(DisplayName = "W")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EHyperTwistMelindaTurnDirection : uint8
|
||||
{
|
||||
Clockwise UMETA(DisplayName = "Clockwise"),
|
||||
CounterClockwise UMETA(DisplayName = "Counter Clockwise")
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelindaCellTurnRequest
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
EHyperTwistMelindaCellAxis RotationAxis = EHyperTwistMelindaCellAxis::X;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
EHyperTwistMelindaTurnDirection Direction = EHyperTwistMelindaTurnDirection::Clockwise;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelindaCellTurnTransformBuildResult
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FHyperTwistTransformation Transformation;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
TArray<FString> Warnings;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bBuilt = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bExactTransform = false;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelindaProjectedCubieFace
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
EHyperTwistMelindaCellAxis Axis = EHyperTwistMelindaCellAxis::X;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bPositiveSide = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
EHyperTwistMelindaStickerColor Color = EHyperTwistMelindaStickerColor::Unknown;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return Color != EHyperTwistMelindaStickerColor::Unknown;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelindaProjectedCubie
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
int32 PositionIndex = INDEX_NONE;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
int32 PieceId = INDEX_NONE;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FIntVector LocalGridCoordinate = FIntVector::ZeroValue;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FString SlotLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FString PieceLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bPieceInSolvedPosition = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
TArray<FHyperTwistMelindaProjectedCubieFace> Faces;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
if (PositionIndex < 0
|
||||
|| PositionIndex >= 16
|
||||
|| PieceId < 0
|
||||
|| PieceId >= 16
|
||||
|| LocalGridCoordinate.X < -1
|
||||
|| LocalGridCoordinate.X > 1
|
||||
|| LocalGridCoordinate.X == 0
|
||||
|| LocalGridCoordinate.Y < -1
|
||||
|| LocalGridCoordinate.Y > 1
|
||||
|| LocalGridCoordinate.Y == 0
|
||||
|| LocalGridCoordinate.Z < -1
|
||||
|| LocalGridCoordinate.Z > 1
|
||||
|| LocalGridCoordinate.Z == 0
|
||||
|| SlotLabel.IsEmpty()
|
||||
|| PieceLabel.IsEmpty()
|
||||
|| bPieceInSolvedPosition != (PositionIndex == PieceId)
|
||||
|| Faces.Num() != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bSeenAxes[4] = {false, false, false, false};
|
||||
for (const FHyperTwistMelindaProjectedCubieFace& Face : Faces)
|
||||
{
|
||||
const int32 AxisIndex = static_cast<int32>(Face.Axis);
|
||||
if (!Face.IsStructurallyValid()
|
||||
|| AxisIndex < 0
|
||||
|| AxisIndex >= 4
|
||||
|| bSeenAxes[AxisIndex])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bSeenAxes[AxisIndex] = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelindaProjectedCell
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
EHyperTwistMelindaCell Cell = EHyperTwistMelindaCell::Outer;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
EHyperTwistMelindaCellAxis FixedAxis = EHyperTwistMelindaCellAxis::W;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bPositiveSide = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FString CellLabel;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FVector DisplayCenter = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
TArray<EHyperTwistMelindaCellAxis> AvailableAxes;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
TArray<FHyperTwistMelindaProjectedCubie> Cubies;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
if (CellLabel.IsEmpty()
|
||||
|| DisplayCenter.ContainsNaN()
|
||||
|| AvailableAxes.Num() != 3
|
||||
|| Cubies.Num() != 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bSeenAvailableAxes[4] = {false, false, false, false};
|
||||
for (const EHyperTwistMelindaCellAxis Axis : AvailableAxes)
|
||||
{
|
||||
const int32 AxisIndex = static_cast<int32>(Axis);
|
||||
if (AxisIndex < 0
|
||||
|| AxisIndex >= 4
|
||||
|| Axis == FixedAxis
|
||||
|| bSeenAvailableAxes[AxisIndex])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bSeenAvailableAxes[AxisIndex] = true;
|
||||
}
|
||||
|
||||
bool bSeenPositions[16] = {
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, false, false, false,
|
||||
false, false, false, false
|
||||
};
|
||||
for (const FHyperTwistMelindaProjectedCubie& Cubie : Cubies)
|
||||
{
|
||||
if (!Cubie.IsStructurallyValid()
|
||||
|| bSeenPositions[Cubie.PositionIndex])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bSeenPositions[Cubie.PositionIndex] = true;
|
||||
for (const FHyperTwistMelindaProjectedCubieFace& Face : Cubie.Faces)
|
||||
{
|
||||
if (!AvailableAxes.Contains(Face.Axis))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelindaCellFirstProjection
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FString ProjectionProfile = TEXT("melinda-2x2x2x2-cell-first-projection-v1");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FHyperTwistPuzzleDefinitionRef Definition;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
TArray<FHyperTwistMelindaProjectedCell> Cells;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
if (ProjectionProfile.IsEmpty()
|
||||
|| !Definition.IsStructurallyValid()
|
||||
|| Cells.Num() != 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bSeenCells[8] = {
|
||||
false, false, false, false,
|
||||
false, false, false, false
|
||||
};
|
||||
for (const FHyperTwistMelindaProjectedCell& Cell : Cells)
|
||||
{
|
||||
const int32 CellIndex = static_cast<int32>(Cell.Cell);
|
||||
if (!Cell.IsStructurallyValid()
|
||||
|| CellIndex < 0
|
||||
|| CellIndex >= 8
|
||||
|| bSeenCells[CellIndex])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bSeenCells[CellIndex] = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistMelindaCellFirstProjectionBuildResult
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FHyperTwistMelindaCellFirstProjection Projection;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
FHyperTwistPuzzleStateValidationResult SourceStateValidation;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
TArray<FString> Warnings;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bProjected = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda")
|
||||
bool bExactProjection = false;
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class UNREALHYPERTWIST_API UHyperTwistMelindaProjectionLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
|
||||
static FString GetCellLabel(EHyperTwistMelindaCell Cell);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
|
||||
static FString GetAxisLabel(EHyperTwistMelindaCellAxis Axis);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
|
||||
static bool IsAxisAvailableForCell(EHyperTwistMelindaCell Cell, EHyperTwistMelindaCellAxis Axis);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
|
||||
static TArray<EHyperTwistMelindaCellAxis> GetAvailableAxesForCell(EHyperTwistMelindaCell Cell);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
|
||||
static FHyperTwistMelindaCellTurnTransformBuildResult BuildCellTurnTransformation(
|
||||
const FHyperTwistPuzzleDefinitionRef& Definition,
|
||||
const FHyperTwistMelindaCellTurnRequest& Request
|
||||
);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Melinda")
|
||||
static FHyperTwistMelindaCellFirstProjectionBuildResult BuildCellFirstProjection(
|
||||
const FHyperTwistPuzzleState& State,
|
||||
float CellCenterSpacing = 120.0f
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "HyperTwistMelindaProjectionOrbitPawn.generated.h"
|
||||
|
||||
class AHyperTwistMelindaProjectionActor;
|
||||
class UCameraComponent;
|
||||
class USceneComponent;
|
||||
class USpringArmComponent;
|
||||
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class UNREALHYPERTWIST_API AHyperTwistMelindaProjectionOrbitPawn : public APawn
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AHyperTwistMelindaProjectionOrbitPawn();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
FVector OrbitFocusPoint = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
bool bFollowActiveProjectionActor = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float InitialArmLength = 980.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float MinimumArmLength = 420.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float MaximumArmLength = 1800.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float ZoomStep = 90.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float OrbitYawDegreesPerPixel = 0.22f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float OrbitPitchDegreesPerPixel = 0.18f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float MinimumPitchDegrees = -80.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float MaximumPitchDegrees = -12.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float InitialYawDegrees = 38.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
float InitialPitchDegrees = -26.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
bool bUseMiddleMouseOrbit = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Melinda|Camera")
|
||||
bool bUseRightMouseOrbitWithShift = false;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera")
|
||||
TObjectPtr<USceneComponent> SceneRoot = nullptr;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera")
|
||||
TObjectPtr<USpringArmComponent> SpringArm = nullptr;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Melinda|Camera")
|
||||
TObjectPtr<UCameraComponent> CameraComponent = nullptr;
|
||||
|
||||
protected:
|
||||
void ApplyOrbitTransform();
|
||||
void RefreshOrbitFocusPointFromProjection();
|
||||
AHyperTwistMelindaProjectionActor* ResolveProjectionActor() const;
|
||||
bool ShouldOrbitFromMouseInput() const;
|
||||
|
||||
float CurrentYawDegrees = 0.0f;
|
||||
float CurrentPitchDegrees = 0.0f;
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,305 @@
|
|||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
|
||||
#include "HyperTwistCore/HyperTwistCoreLibrary.h"
|
||||
#include "HyperTwistSimulation/HyperTwistMelindaProjectionLibrary.h"
|
||||
#include "HyperTwistSimulation/HyperTwistSimulationLibrary.h"
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
|
||||
namespace HyperTwistMelindaPhase6AContractTestInternal
|
||||
{
|
||||
FString SerializeState(const FHyperTwistPuzzleState& State)
|
||||
{
|
||||
return UHyperTwistContractLibrary::SerializePuzzleStateToJson(State);
|
||||
}
|
||||
|
||||
bool ApplyExactTransform(
|
||||
FAutomationTestBase& Test,
|
||||
const FHyperTwistPuzzleState& State,
|
||||
const FHyperTwistTransformation& Transformation,
|
||||
FHyperTwistPuzzleState& OutState
|
||||
)
|
||||
{
|
||||
const FHyperTwistApplyTransformationResult Applied =
|
||||
UHyperTwistCoreLibrary::ApplyTransformation(State, Transformation);
|
||||
Test.TestTrue(TEXT("Melinda Phase 6A transforms must apply."), Applied.bApplied);
|
||||
Test.TestTrue(
|
||||
TEXT("Melinda Phase 6A transforms must produce exact state updates."),
|
||||
Applied.bExactStateUpdate
|
||||
);
|
||||
if (!Applied.bApplied || !Applied.bExactStateUpdate)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistPuzzleStateValidationResult Validation =
|
||||
UHyperTwistCoreLibrary::ValidatePuzzleState(Applied.State);
|
||||
Test.TestTrue(
|
||||
TEXT("Applied Melinda Phase 6A states must remain solvable."),
|
||||
Validation.bIsSolvable
|
||||
);
|
||||
OutState = Applied.State;
|
||||
return Validation.bIsSolvable;
|
||||
}
|
||||
|
||||
EHyperTwistMelindaTurnDirection InvertDirection(
|
||||
const EHyperTwistMelindaTurnDirection Direction
|
||||
)
|
||||
{
|
||||
return Direction == EHyperTwistMelindaTurnDirection::Clockwise
|
||||
? EHyperTwistMelindaTurnDirection::CounterClockwise
|
||||
: EHyperTwistMelindaTurnDirection::Clockwise;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistMelindaPhase6ACellTurnContractTest,
|
||||
"HyperTwist.CleanRoom.HactarCE.Phase6A.CellTurnContract",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistMelindaPhase6ACellTurnContractTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FHyperTwistPuzzleDefinitionRef Definition =
|
||||
UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition();
|
||||
const FHyperTwistPuzzleState SolvedState =
|
||||
UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
|
||||
const FString SerializedSolvedState =
|
||||
HyperTwistMelindaPhase6AContractTestInternal::SerializeState(SolvedState);
|
||||
|
||||
static const EHyperTwistMelindaCell Cells[] = {
|
||||
EHyperTwistMelindaCell::Left,
|
||||
EHyperTwistMelindaCell::Right,
|
||||
EHyperTwistMelindaCell::Back,
|
||||
EHyperTwistMelindaCell::Front,
|
||||
EHyperTwistMelindaCell::Down,
|
||||
EHyperTwistMelindaCell::Up,
|
||||
EHyperTwistMelindaCell::Inner,
|
||||
EHyperTwistMelindaCell::Outer
|
||||
};
|
||||
static const EHyperTwistMelindaTurnDirection Directions[] = {
|
||||
EHyperTwistMelindaTurnDirection::Clockwise,
|
||||
EHyperTwistMelindaTurnDirection::CounterClockwise
|
||||
};
|
||||
|
||||
for (const EHyperTwistMelindaCell Cell : Cells)
|
||||
{
|
||||
const TArray<EHyperTwistMelindaCellAxis> AvailableAxes =
|
||||
UHyperTwistMelindaProjectionLibrary::GetAvailableAxesForCell(Cell);
|
||||
TestEqual(TEXT("Each Phase 6A cell should expose exactly three twist axes."), AvailableAxes.Num(), 3);
|
||||
|
||||
for (const EHyperTwistMelindaCellAxis Axis : AvailableAxes)
|
||||
{
|
||||
for (const EHyperTwistMelindaTurnDirection Direction : Directions)
|
||||
{
|
||||
FHyperTwistMelindaCellTurnRequest Request;
|
||||
Request.Cell = Cell;
|
||||
Request.RotationAxis = Axis;
|
||||
Request.Direction = Direction;
|
||||
|
||||
const FHyperTwistMelindaCellTurnTransformBuildResult BuildResult =
|
||||
UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
|
||||
Definition,
|
||||
Request
|
||||
);
|
||||
TestTrue(TEXT("Each Phase 6A cell turn should build."), BuildResult.bBuilt);
|
||||
TestTrue(TEXT("Each Phase 6A cell turn should remain exact."), BuildResult.bExactTransform);
|
||||
TestTrue(
|
||||
TEXT("Each Phase 6A cell turn must produce structural transform data."),
|
||||
BuildResult.Transformation.IsStructurallyValid()
|
||||
);
|
||||
|
||||
FHyperTwistPuzzleState OnceTurnedState;
|
||||
if (!HyperTwistMelindaPhase6AContractTestInternal::ApplyExactTransform(
|
||||
*this,
|
||||
SolvedState,
|
||||
BuildResult.Transformation,
|
||||
OnceTurnedState
|
||||
))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestFalse(
|
||||
TEXT("Any Phase 6A cell turn should disturb the solved state."),
|
||||
UHyperTwistCoreLibrary::IsSolved(OnceTurnedState)
|
||||
);
|
||||
|
||||
FHyperTwistMelindaCellTurnRequest InverseRequest = Request;
|
||||
InverseRequest.Direction =
|
||||
HyperTwistMelindaPhase6AContractTestInternal::InvertDirection(Direction);
|
||||
const FHyperTwistMelindaCellTurnTransformBuildResult InverseBuildResult =
|
||||
UHyperTwistMelindaProjectionLibrary::BuildCellTurnTransformation(
|
||||
Definition,
|
||||
InverseRequest
|
||||
);
|
||||
TestTrue(TEXT("Inverse Phase 6A turns should build."), InverseBuildResult.bBuilt);
|
||||
TestTrue(TEXT("Inverse Phase 6A turns should remain exact."), InverseBuildResult.bExactTransform);
|
||||
|
||||
FHyperTwistPuzzleState RecoveredState;
|
||||
if (!HyperTwistMelindaPhase6AContractTestInternal::ApplyExactTransform(
|
||||
*this,
|
||||
OnceTurnedState,
|
||||
InverseBuildResult.Transformation,
|
||||
RecoveredState
|
||||
))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
TEXT("A turn followed by its inverse must recover the solved Melinda state."),
|
||||
HyperTwistMelindaPhase6AContractTestInternal::SerializeState(RecoveredState),
|
||||
SerializedSolvedState
|
||||
);
|
||||
|
||||
FHyperTwistPuzzleState QuadTurnState = SolvedState;
|
||||
for (int32 TurnIndex = 0; TurnIndex < 4; ++TurnIndex)
|
||||
{
|
||||
FHyperTwistPuzzleState NextState;
|
||||
if (!HyperTwistMelindaPhase6AContractTestInternal::ApplyExactTransform(
|
||||
*this,
|
||||
QuadTurnState,
|
||||
BuildResult.Transformation,
|
||||
NextState
|
||||
))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QuadTurnState = NextState;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
TEXT("Four quarter turns on one Phase 6A cell axis must return to identity."),
|
||||
HyperTwistMelindaPhase6AContractTestInternal::SerializeState(QuadTurnState),
|
||||
SerializedSolvedState
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistMelindaPhase6ACellFirstProjectionContractTest,
|
||||
"HyperTwist.CleanRoom.HactarCE.Phase6A.CellFirstProjectionContract",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistMelindaPhase6ACellFirstProjectionContractTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FHyperTwistPuzzleState SolvedState =
|
||||
UHyperTwistContractLibrary::MakeSampleHyperPuzzleState();
|
||||
const FHyperTwistMelindaCellFirstProjectionBuildResult SolvedProjectionResult =
|
||||
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(SolvedState, 120.0f);
|
||||
|
||||
TestTrue(TEXT("The solved Melinda state must project in Phase 6A."), SolvedProjectionResult.bProjected);
|
||||
TestTrue(TEXT("The solved Melinda projection must remain exact."), SolvedProjectionResult.bExactProjection);
|
||||
TestTrue(
|
||||
TEXT("The solved Melinda projection must stay structurally valid."),
|
||||
SolvedProjectionResult.Projection.IsStructurallyValid()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The Phase 6A projection profile must stay stable."),
|
||||
SolvedProjectionResult.Projection.ProjectionProfile,
|
||||
FString(TEXT("melinda-2x2x2x2-cell-first-projection-v1"))
|
||||
);
|
||||
|
||||
TMap<int32, int32> PositionAppearanceCount;
|
||||
TSet<FString> CellLabels;
|
||||
for (const FHyperTwistMelindaProjectedCell& CellProjection :
|
||||
SolvedProjectionResult.Projection.Cells)
|
||||
{
|
||||
CellLabels.Add(CellProjection.CellLabel);
|
||||
TestEqual(TEXT("Each projected Phase 6A cell should expose eight cubies."), CellProjection.Cubies.Num(), 8);
|
||||
TestEqual(
|
||||
TEXT("Each projected Phase 6A cell should expose exactly three available axes."),
|
||||
CellProjection.AvailableAxes.Num(),
|
||||
3
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("The fixed axis must not be listed as an available Phase 6A turn axis."),
|
||||
CellProjection.AvailableAxes.Contains(CellProjection.FixedAxis)
|
||||
);
|
||||
for (const EHyperTwistMelindaCellAxis Axis : CellProjection.AvailableAxes)
|
||||
{
|
||||
TestTrue(
|
||||
TEXT("Every listed Phase 6A axis must be turnable for its cell."),
|
||||
UHyperTwistMelindaProjectionLibrary::IsAxisAvailableForCell(CellProjection.Cell, Axis)
|
||||
);
|
||||
}
|
||||
|
||||
for (const FHyperTwistMelindaProjectedCubie& CubieProjection : CellProjection.Cubies)
|
||||
{
|
||||
PositionAppearanceCount.FindOrAdd(CubieProjection.PositionIndex)++;
|
||||
TestEqual(
|
||||
TEXT("Each projected cubie should expose three visible faces in the cell-first shell."),
|
||||
CubieProjection.Faces.Num(),
|
||||
3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TestEqual(TEXT("Phase 6A should expose all eight named cells."), CellLabels.Num(), 8);
|
||||
for (int32 PositionIndex = 0; PositionIndex < 16; ++PositionIndex)
|
||||
{
|
||||
TestEqual(
|
||||
TEXT("Each 4D cubie should appear in four projected cells."),
|
||||
PositionAppearanceCount.FindRef(PositionIndex),
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
const FHyperTwistRandomStateGenerationResult RandomStateResult =
|
||||
UHyperTwistCoreLibrary::GenerateRandomPuzzleState(
|
||||
UHyperTwistContractLibrary::MakeSampleHyperPuzzleDefinition(),
|
||||
62026
|
||||
);
|
||||
TestTrue(TEXT("Phase 6A random-state coverage must begin with an exact Melinda state."), RandomStateResult.bGenerated);
|
||||
TestTrue(TEXT("The generated Phase 6A random state must remain solvable."), RandomStateResult.Validation.bIsSolvable);
|
||||
|
||||
const FHyperTwistMelindaCellFirstProjectionBuildResult RandomProjectionResult =
|
||||
UHyperTwistMelindaProjectionLibrary::BuildCellFirstProjection(
|
||||
RandomStateResult.State,
|
||||
120.0f
|
||||
);
|
||||
TestTrue(TEXT("Random Melinda states must project in Phase 6A."), RandomProjectionResult.bProjected);
|
||||
TestTrue(TEXT("Random Melinda projections must remain exact."), RandomProjectionResult.bExactProjection);
|
||||
TestTrue(
|
||||
TEXT("Random Melinda projections must remain structurally valid."),
|
||||
RandomProjectionResult.Projection.IsStructurallyValid()
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistMelindaPhase6ASceneContextContractTest,
|
||||
"HyperTwist.CleanRoom.HactarCE.Phase6A.SceneContextContract",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistMelindaPhase6ASceneContextContractTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FHyperTwistSimulationSceneContext SceneContext =
|
||||
UHyperTwistSimulationLibrary::MakeMelindaCellFirstSceneContext();
|
||||
|
||||
TestTrue(TEXT("The Phase 6A scene context should be structurally valid."), SceneContext.IsStructurallyValid());
|
||||
TestTrue(TEXT("The Phase 6A scene context should be renderable."), UHyperTwistSimulationLibrary::CanRenderSceneContext(SceneContext));
|
||||
TestEqual(
|
||||
TEXT("The Phase 6A scene context should use the cell-first projection profile."),
|
||||
SceneContext.ProjectionSettings.ProjectionProfile,
|
||||
FString(TEXT("melinda-2x2x2x2-cell-first-projection-v1"))
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The Phase 6A scene context should advertise the owned procedural runtime."),
|
||||
SceneContext.RenderStateProfile,
|
||||
FString(TEXT("melinda-cell-first-procedural-runtime-v1"))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue