Steamwrecked

Project Overview
Steamwrecked is a survival, exploration we created during our 3rd game project at Future Games Stockholm. The game takes place in a deserted land, filled with natural hazards, as well as metallic, automatic lifeforms created by the previous inhabitants. Our main character, who crash landed in this land, have to gather resource and blueprint to survive, upgrade, and unveil the truth behind the fall of the civilization who once lived here.
My Contributions
During this project, my main contribution is the item system, and a custom editor for the item in order to speed up the production of in-game content. My second contribution is the airship's movement, and a modular mounting system which simplifies the process to create new mountables in the future. Later in the project, I also contributed towards the refactoring of the inventory system in order to decouple the various parts of the system, centralized logic into one area, and also fixed many bugs that came with the initial design.
Role
System & Tools Programmer
Duration
7 Weeks
Team Size
17
Genre
Survival, Exploration
One of my main contribution for this project is setting up the structure for the various items. Each item consists of two components, a DataAsset which holds all the constant values relevant to an item, and a WorldItem which is the visible item that can be placed in the world, and interacted by the player
UCLASS(BlueprintType)
class PROJECTSANDWALKER_API UInventoryItem : public UDataAsset
{
	GENERATED_BODY()

	//
	// Properties
	//
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	FString ItemName;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	FString ItemDescription;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	UTexture2D* ItemIcon;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties",
		meta=(Bitmask, BitmaskEnum = "EInventoryItemType"))
	int32 InventoryItemType;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	FIntPoint Dimension;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	FIntPoint ItemPivot;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	TArray<FIntPoint> BlocksOffsetFromPivot;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	TArray<FExtraData> ExtraData;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	TSubclassOf<AWorldItem> WorldItemClass;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	bool bIsRecipe;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	UTexture2D* CraftedItemIcon;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	TArray<FCraftingMaterialEntry> Material;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	UInventoryItem* CraftedItem;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties")
	bool bIsTool;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory Item | Properties", meta = (EditCondition = "bIsTool"))
	TSubclassOf<AToolBase> ToolClass;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Properties")
	FVector2D IconSize;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Properties")
	FVector2D IconPosition;

#pragma region EDITOR
#if WITH_EDITORONLY_DATA
	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Editor")
	FIntPoint TopLeft;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Editor")
	FIntPoint BottomRight;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = true), Category="Inventory Item | Editor")
	UBlueprint* BlueprintAsset;
#endif

#if WITH_EDITOR
	UFUNCTION(BlueprintCallable)
	void SetupInstance(const FString& Name, const FString& Description, UTexture2D* Icon, const FVector2D& Size,
	                   const FVector2D& Position, const FIntPoint& Pivot, const TArray<FIntPoint>& Blocks,
	                   const int32& Type, const TArray<FCraftingMaterialEntry>& CraftingIngredients);

	UFUNCTION(BlueprintCallable)
	void SetDimension(FIntPoint NewTopLeft, FIntPoint NewBottomRight);
#endif
#pragma endregion
};
UCLASS(Blueprintable, BlueprintType)
class PROJECTSANDWALKER_API AWorldItem : public AActor
{
	GENERATED_BODY()

public:
	AWorldItem();

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory|Item")
	UInventoryItem* InventoryItem;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventory|Item")
	FGuid Guid;

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;
};
In our game, we have a tetris style inventory system where each item takes up different number of tile in a grid inventory. While modifying the occupying tiles and the icons of items through numbers is possible, it is time consuming. With this in mind, I created an item editor with the following features: A detail section to modify the basic information related to an item; A grid section where designers can visualize the icon against the grid, and adjust the size and location of the icon, and select the tiles that the item will occupy.
The airship is an essential part of the game, it is the main means of transportation for the player to explore buildings in higher altitudes. A lot of emphasis is put into creating the airship in order to make the movement more smooth and floaty to mimic the feeling of controlling an airship
// Sets default values
ASteamPunkShip::ASteamPunkShip()
{
	BoxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
	SetRootComponent(BoxComp);

	Generator = CreateDefaultSubobject<UChildActorComponent>("Generator");
	Generator->SetupAttachment(RootComponent);

	CameraSpringArm = CreateDefaultSubobject<USpringArmComponent>("Spring Arm");
	CameraSpringArm->SetupAttachment(RootComponent);

	ShipCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ActualCamera"));
	ShipCamera->SetupAttachment(CameraSpringArm, USpringArmComponent::SocketName);

	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	SetActorRotation(GetActorRotation() + FRotator(0, 90, 0));
}

void ASteamPunkShip::SetAdditionalVelocity(float InAdditionalVelocity)
{
	AdditionalVelocity = InAdditionalVelocity;
}

void ASteamPunkShip::AddAdditionalVelocity(float InAdditionalVelocity)
{
	AdditionalVelocity += InAdditionalVelocity;
}

float ASteamPunkShip::GetAdditionalVelocity()
{
	return AdditionalVelocity;
}

void ASteamPunkShip::SetAdditionalVector(FVector& InAdditionalVector)
{
	AdditionalVector = InAdditionalVector;
}

void ASteamPunkShip::ResetAdditionalVector()
{
	AdditionalVector = FVector(0, 0, 0);
}

void ASteamPunkShip::ShipTakeDamage(float Amount, FVector ImpulseDirection)
{
	OnShipDamagedDelegate.Broadcast();

	ImpulseForce = ImpulseDirection * DamageImpactForce;

	float NewAmount = Health - Amount;

	Health = FMath::Max(NewAmount, 0);

	if (Health == 0)
	{
		IsFunctional = false;
		OnStateChangedDelegate.Broadcast(IsFunctional);
	}
}

void ASteamPunkShip::SetHealth(float Amount)
{
	Health = FMath::Min(Amount, MaxHealth);

	if (Health > 0)
	{
		IsFunctional = true;
		OnStateChangedDelegate.Broadcast(IsFunctional);
	}
}

float ASteamPunkShip::GetHealth()
{
	return Health;
}
void ASteamPunkShip::Move_Implementation(const FVector& Value)
{
	if (Fuel <= 0 || Accelaration == 0 || !IsFunctional)
	{
		SteerDirection = 0;
		ForwardAccel = 0;
		IsAccelerating = false;
		return;
	}

	if (Controller != nullptr)
	{
		SteerDirection = Value.X;
		ForwardAccel = Value.Y;

		if (Value.Y != 0)
		{
			IsAccelerating = true;
		}
		if (Value.X != 0)
		{
			IsAccelerating = true;
		}
	}
}

void ASteamPunkShip::MoveStop_Implementation()
{
	IsAccelerating = false;

	ForwardAccel = 0;
	SteerDirection = 0;
}

void ASteamPunkShip::Look_Implementation(const FVector& Value)
{
	if (Controller != nullptr)
	{
		// add yaw and pitch input to controller
		AddControllerYawInput(Value.X * RotationMult);
		AddControllerRollInput(Value.Y * RotationMult);
	}
}

void ASteamPunkShip::JumpTrigger_Implementation()
{
	if (Fuel <= 0 || Accelaration == 0 || !IsFunctional)
	{
		AccelUp = false;
		IsAccelerating = false;
		return;
	}

	AccelUp = true;

	IsAccelerating = true;
}

void ASteamPunkShip::JumpStop_Implementation()
{
	AccelUp = false;
	IsAccelerating = false;

}

void ASteamPunkShip::Crouch_Implementation()
{
	if (Accelaration == 0 || !IsFunctional)
	{
		return;
	}

	AccelDown = true;
}

void ASteamPunkShip::CrouchStop_Implementation()
{
	AccelDown = false;
}

void ASteamPunkShip::Interact_Implementation()
{
	if (AController* DriverController = GetController())
	{
		Driver->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
		Driver->SetActorHiddenInGame(false);
		Driver->SetActorLocation(CameraSpringArm->GetComponentLocation());
		DriverController->Possess(Driver);
		Driver = nullptr;
	}

	UE_LOG(LogTemp, Display, TEXT("Interact"));
}
void ASteamPunkShip::UpdateImpulse(const float& DeltaTime)
{
	if (ImpulseForce.Size() <= 0)
		return;

	SetActorLocation(GetActorLocation() + (ImpulseForce * DeltaTime), true);

	FVector NewForce = (ImpulseForce - (ImpulseForce.GetSafeNormal() * ImpulseReduceRate * DeltaTime));

	if (NewForce.Dot(ImpulseForce) < 0)
		ImpulseForce = FVector(0, 0, 0);
	else
		ImpulseForce = NewForce;
}

void ASteamPunkShip::UpdateVerticalMovement(const float& DeltaTime)
{
	if (AccelUp)
		VerticalVelocity = FMath::Min(VerticalVelocity + (UpwardAccelaration * DeltaTime), MaxUpwardVelocity);

	if (AccelDown)
		VerticalVelocity = FMath::Max(VerticalVelocity + (DownwardAccelaration * DeltaTime), MaxDownwardVelocity);

	if (VerticalVelocity == 0)
		return;

	if (VerticalVelocity > 0 && GetActorLocation().Z > HeightLimit)
	{
		VerticalVelocity = FMath::Max(VerticalVelocity - SuppressionForce * DeltaTime, 0);
	}

	SetActorLocation(GetActorLocation() + (GetActorUpVector() * VerticalVelocity * DeltaTime), true);

	if (VerticalVelocity > 0)
	{
		VerticalVelocity = (VerticalVelocity + DownwardDrag * DeltaTime < 0)
							   ? 0
							   : VerticalVelocity + DownwardDrag * DeltaTime;
	}
	else
	{
		VerticalVelocity = (VerticalVelocity + UpwardDrag * DeltaTime > 0)
							   ? 0
							   : VerticalVelocity + UpwardDrag * DeltaTime;
	}
}

void ASteamPunkShip::UpdateSteering(const float& DeltaTime)
{
	if (SteerDirection != 0)
		SteeringSpeed += DeltaTime * SteeringPower * SteerDirection;

	if (SteeringSpeed == 0)
		return;

	if (SteeringSpeed > 0)
	{
		SteeringSpeed = (SteeringSpeed - SteeringDrag * DeltaTime <= 0)
							   ? 0
							   : SteeringSpeed - SteeringDrag * DeltaTime;
	}
	else
	{
		SteeringSpeed = (SteeringSpeed + SteeringDrag * DeltaTime >= 0)
							   ? 0
							   : SteeringSpeed + SteeringDrag * DeltaTime;
	}

	SetActorRotation(GetActorRotation() + FRotator(0, SteeringSpeed * DeltaTime, 0));
}

void ASteamPunkShip::UpdateMovement(const float& DeltaTime)
{
	if (ForwardAccel != 0)
	{
		if (ForwardAccel < 0)
		{
			if (Velocity > 0)
			{
				Velocity -= Accelaration * DeltaTime;
			}
			Velocity = FMath::Max(0, Velocity);
		}
		else if (Velocity < MaxVelocity)
		{
			Velocity += Accelaration * DeltaTime ;
		}
	}

	FVector Movement = FVector::ZeroVector;

	if (Velocity == 0)
		return;

	Movement += GetActorRightVector() * Velocity * DeltaTime;
	Movement += AdditionalVector * AdditionalVelocity * DeltaTime;
	SetActorLocation(GetActorLocation() + Movement, true);

	Velocity = (Velocity + Drag * DeltaTime <= 0) ? 0 : Velocity + Drag * DeltaTime;
}

// Called every frame
void ASteamPunkShip::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	UpdateSteering(DeltaTime);
	UpdateVerticalMovement(DeltaTime);
	UpdateMovement(DeltaTime);
	UpdateImpulse(DeltaTime);

	if (PotentialFuel <= 0)
		return;

	if (IsAccelerating)
	{
		Fuel -= DeltaTime;
		//Fuel = FMath::Max(Fuel, 0);

		PotentialFuel -= DeltaTime;
		PotentialFuel = FMath::Max(PotentialFuel, 0);

		for (int i = 0; i < PropellerRibbonNiagaraSystems.Num(); i++)
		{
			if (!PropellerRibbonNiagaraSystems[i]->IsActive())
			{
				PropellerRibbonNiagaraSystems[i]->Activate();
			}
		}
	}
	else
	{
		for (int i = 0; i < PropellerRibbonNiagaraSystems.Num(); i++)
		{
			if (PropellerRibbonNiagaraSystems[i]->IsActive())
			{
				PropellerRibbonNiagaraSystems[i]->Deactivate();
			}
		}
	}

	if (Fuel <= 0)
	{
		if (UInventoryComponent* Inventory = Generator->GetChildActor()->GetComponentByClass<UInventoryComponent>())
		{
			TArray<FInventoryItemPosition> Items = Inventory->GetAllItems();
			FInventoryItemPosition* ItemPos = &Items[0];
			if (!ItemPos)
			{
				return;
			}

			if (ItemPos->Item->InventoryItemType & static_cast<int>(EInventoryItemType::Burnable))
			{
				for (auto& ExtraData : ItemPos->Item->ExtraData)
				{
					if (ExtraData.Type != 6)
					{
						continue;
					}

					Inventory->TryRemoveItem(ItemPos->Item);

					if (!Inventory->GetAllItems().IsEmpty())
						Fuel = ExtraData.Value + Fuel;

					if (UInventoryManager* InventoryManager = Inventory->InventoryManager)
					{
						InventoryManager->UpdateProgressBar(Inventory, PotentialFuel / MaxFuel);
					}

					break;
				}
			}

			Inventory->RefreshUIElements();
		}
	}
}
During the implementation of the airship, we also discussed the possibility to have more mountables in the game, such as a sand-glider for mid-distance exploration, and a harpoon cannon to hunt the creatures in the game. With the current controlling scheme, implementing future mountables would require a lot of repeated work. For this reason, I also re-planned the control scheme.
UINTERFACE(MinimalAPI, Blueprintable)
class UMountableBase : public UInterface
{
	GENERATED_BODY()
};

class PROJECTSANDWALKER_API IMountableBase
{
	GENERATED_BODY()

public:
	// Sets default values for this pawn's properties
	IMountableBase();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void PrimaryAction();
	virtual void PrimaryAction_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void PrimaryActionEnd();
	virtual void PrimaryActionEnd_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void SecondaryAction();
	virtual void SecondaryAction_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void Move(const FVector& Value);
	virtual void Move_Implementation(const FVector& Value);

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void MoveStop();
	virtual void MoveStop_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void Look(const FVector& Value);
	virtual void Look_Implementation(const FVector& Value);

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void RunStart();
	virtual void RunStart_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void RunStop();
	virtual void RunStop_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void JumpTrigger();
	virtual void JumpTrigger_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void JumpStart();
	virtual void JumpStart_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void JumpStop();
	virtual void JumpStop_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void Crouch();
	virtual void Crouch_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void CrouchStop();
	virtual void CrouchStop_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void Interact();
	virtual void Interact_Implementation();

	UFUNCTION(BlueprintNativeEvent, Category = "Input")
	void ToggleInventory();
	virtual void ToggleInventory_Implementation();
};
void AMainController::BeginPlay()
{
	Super::BeginPlay();

	//Add Input Mapping Context
	if (AMainController* PlayerController = Cast<AMainController>(this))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<
			UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(PlayerMappingContext, 0);
		}
		if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(InputComponent))
		{
			// Actions
			EnhancedInputComponent->BindAction(PrimaryActionAction, ETriggerEvent::Triggered, this,
			                                   &AMainController::PrimaryAction);
			EnhancedInputComponent->BindAction(PrimaryActionAction, ETriggerEvent::Completed, this,
			                                   &AMainController::PrimaryActionEnd);

			EnhancedInputComponent->BindAction(SecondaryActionAction, ETriggerEvent::Triggered, this,
			                                   &AMainController::SecondaryAction);

			//Movement
			EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMainController::Move);
			EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AMainController::MoveStop);

			//Looking
			EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMainController::Look);

			//Running
			EnhancedInputComponent->BindAction(RunningAction, ETriggerEvent::Started, this, &AMainController::RunStart);
			EnhancedInputComponent->BindAction(RunningAction, ETriggerEvent::Completed, this,
			                                   &AMainController::RunStop);

			//Jumping
			EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this,
			                                   &AMainController::JumpTrigger);
			EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AMainController::JumpStart);
			EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AMainController::JumpStop);

			//Crouch
			EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Triggered, this, &AMainController::Crouch);
			EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AMainController::CrouchStop);

			//Interact
			EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this,
			                                   &AMainController::Interact);

			//Inventory
			EnhancedInputComponent->BindAction(InventoryAction, ETriggerEvent::Started, this,
			                                   &AMainController::ToggleInventory);
		}
	}
}

void AMainController::OnPossess(APawn* inPawn)
{
	Super::OnPossess(inPawn);

	if (!inPawn->GetClass()->ImplementsInterface(UMountableBase::StaticClass()))
	{
		bImplementsInterface = false;
		return;
	}

	bImplementsInterface = true;
}

void AMainController::PrimaryAction()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_PrimaryAction(GetPawn());
}

void AMainController::PrimaryActionEnd()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_PrimaryActionEnd(GetPawn());
}

void AMainController::SecondaryAction()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_SecondaryAction(GetPawn());
}

void AMainController::Move(const FInputActionValue& Value)
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_Move(GetPawn(), Value.Get<FVector>());
}

void AMainController::MoveStop()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_MoveStop(GetPawn());
}

void AMainController::Look(const FInputActionValue& Value)
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_Look(GetPawn(), Value.Get<FVector>());
}

void AMainController::RunStart()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_RunStart(GetPawn());
}

void AMainController::RunStop()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_RunStop(GetPawn());
}

void AMainController::JumpTrigger()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_JumpTrigger(GetPawn());
}

void AMainController::JumpStart()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_JumpStart(GetPawn());
}

void AMainController::JumpStop()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_JumpStop(GetPawn());
}

void AMainController::Crouch()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_Crouch(GetPawn());
}

void AMainController::CrouchStop()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_CrouchStop(GetPawn());
}

void AMainController::Interact()
{
	if (!bImplementsInterface)
	{
		return;
	}

	IMountableBase::Execute_Interact(GetPawn());
}

void AMainController::ToggleInventory()
{
	if (!bImplementsInterface || bIsDead)
	{
		return;
	}

	IMountableBase::Execute_ToggleInventory(GetPawn());
}

void AMainController::SetIsDead(bool b)
{
	bIsDead = b;
}
void UItemWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	Super::NativeOnMouseEnter(InGeometry, InMouseEvent);

	for (UBorder* BorderWidget : BorderWidgets)
	{
		if (BorderWidget)
		{
			BorderWidget->SetBrushColor(BackgroundHoverColor);
		}
	}

	OnMouseEnterDelegate.Execute(this, InventoryItemPosition.Item);
}

void UItemWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
	Super::NativeOnMouseLeave(InMouseEvent);

	for (UBorder* BorderWidget : BorderWidgets)
	{
		if (BorderWidget)
		{
			BorderWidget->SetBrushColor(BackgroundColor);
		}
	}

	OnMouseLeaveDelegate.Execute();
}

void UItemWidget::NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent,
                                       UDragDropOperation*& OutOperation)
{
	if (!bCanRemoveItem)
	{
		Debug::LogWarning(TEXT("Item is not droppable, cannot start drag operation."), true, 2);
		// Ensure focus is cleared so the grid doesn't keep keyboard focus after a blocked drag
		UWidgetBlueprintLibrary::SetFocusToGameViewport();
		return;
	}

	for (UBorder* BorderWidget : BorderWidgets)
	{
		if (BorderWidget)
		{
			BorderWidget->SetBrushColor(BackgroundPressedColor);
		}
	}

	UDragDropOperation* DragOperation = NewObject<UDragDropOperation>();

	// Create drag info object
	UDragInfo* DragInfo = NewObject<UDragInfo>();
	DragInfo->ItemGridPosition = InventoryItemPosition;

	// --- Calculate grab offset ---
	// Get mouse position relative to this widget
	FVector2D LocalMousePos = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
	int32 LocalTileX = FMath::FloorToInt(LocalMousePos.X / TileSize);
	int32 LocalTileY = FMath::FloorToInt(LocalMousePos.Y / TileSize);
	DragInfo->GrabOffset = FIntPoint(LocalTileX, LocalTileY);

	// Start the drag with the current rotation of this instance
	DragInfo->Rotation = InventoryItemPosition.Rotation;

	// Set drag operation properties
		DragOperation->Payload = DragInfo;
	DragOperation->DefaultDragVisual = this;
	DragOperation->Pivot = EDragPivot::MouseDown;

	OutOperation = DragOperation;

	Super::NativeOnDragDetected(InGeometry, InMouseEvent, OutOperation);

	OnItemDragDetectedDelegate.Execute(this, InventoryItemPosition);
}

FReply UItemWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	if (InMouseEvent.IsShiftDown())
	{
		OnMouseButtonUpDelegate.Execute(this, InventoryItemPosition, true);
		return FReply::Handled();
	}

	OnMouseButtonUpDelegate.Execute(this, InventoryItemPosition, false);
	return FReply::Handled();
}

FReply UItemWidget::NativeOnMouseButtonDoubleClick(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	OnMouseButtonDoubleClickDelegate.Execute(this, InventoryItemPosition);

	return FReply::Handled();
}
// This function is called when the drag operation is detected to be stopped outside the widget
bool UInventoryGridWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent,
                                        UDragDropOperation* InOperation)
{
	if (InOperation->Payload)
	{
		UDragInfo* DragData = Cast<UDragInfo>(InOperation->Payload);
		if (!DragData)
		{
			return false;
		}

		UInventoryItem* DraggedItem = DragData->ItemGridPosition.Item;

		FInventoryItemPosition ItemPosition;
		ItemPosition.Item = DraggedItem;
		ItemPosition.Position = DraggedItemTopLeftTile;
		// Ensure rotation is reduced/consistent
		ItemPosition.Rotation = InventoryRotationUtils::ReduceRotationForItem(DraggedItem, DragData->Rotation);

		Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);

		OnItemDropped.Execute(this, ItemPosition);
		bDrawDropLocation = false;

		// Remove keyboard focus so TAB can close inventory again
		UWidgetBlueprintLibrary::SetFocusToGameViewport();

		return true;
	}
	return false;
}
bool UInventoryWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent,
                                    UDragDropOperation* InOperation)
{
	if (InOperation->Payload)
	{
		UDragInfo* DragData = Cast<UDragInfo>(InOperation->Payload);
		if (!DragData)
		{
			return false;
		}

		UInventoryItem* DraggedItem = DragData->ItemGridPosition.Item;

		Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);

		OnItemDropped.Execute(this, DraggedItem);
		return true;
	}
	return false;
}
void UInventoryManager::BeginPlay()
{
	// Set player reference
	Player = GetOwner<AMainCharacterClass>();

	// Set player controller reference
	if (AController* Controller = Player->GetController())
	{
		//This might end up with nullptr even if there is a controller. and this might never happend if a player doesn't have a controller
		PlayerController = Cast<APlayerController>(Controller);
	}
    
	//Create inventory widget
	if (!InventoryWidgetClass)
	{
		Debug::LogWarning("InventoryWidgetClass is not set in InventoryManager. Please set it in the editor or code.",
					  true, 2);
		return;
	}
	if (!PlayerController)
	{
		Debug::LogWarning("Tried to create inventory widget while not having a valid player controller line 89 on Inventory manager. cpp",
				  true, 5);
		return;
	}
	InventoryWidget = CreateWidget<UInventoryWidget>(PlayerController, InventoryWidgetClass);
	InventoryWidget->OnItemDropped.BindDynamic(this, &UInventoryManager::OnItemDroppedFromInventory);
	InventoryWidget->SetVisibility(ESlateVisibility::Hidden);
	InventoryWidget->AddToViewport();
}

void UInventoryManager::OpenComplimentaryInventory(UInventoryComponent* ComplimentaryInventory)
{
	if (!ComplimentaryInventory)
	{
		UE_LOG(LogTemp, Warning, TEXT("Invalid inventory component for opening complimentary inventory."));
		return;
	}

	ComplimentaryInventory->InventoryManager = this;

	if (InventoryWidget->IsVisible())
	{
		// if we only have 1 inventory, we should still open the complimentary inventory
		if (LinkageList.Num() == 1)
		{
			AddInventoryGridWidget(ComplimentaryInventory);
			return;
		}
		ToggleInventory();
		return;
	}

	ToggleInventory();
	AddInventoryGridWidget(ComplimentaryInventory);
}

UInventoryGridWidget* UInventoryManager::AddInventoryGridWidget(UInventoryComponent* InventoryComponent)
{
	if (!InventoryGridWidgetClass || !InventoryWidget)
	{
		Debug::LogWarning("InventoryGridWidgetClass or InventoryWidget is not set in InventoryManager. Please check your setup.",true, 2);
		return nullptr;
	}

	if (!InventoryComponent)
	{
		Debug::LogWarning("InventoryComponent is null. Cannot add inventory grid.", true, 2);
		return nullptr;
	}

	// Create the grid widget
	UInventoryGridWidget* GridWidget = CreateWidget<UInventoryGridWidget>(InventoryWidget, InventoryGridWidgetClass);
	if (!GridWidget)
	{
		Debug::LogWarning("Failed to create InventoryGridWidget. Please check your InventoryGridWidgetClass.", true, 2);
		return nullptr;
	}

	GridWidget->OnItemDropped.BindDynamic(this, &UInventoryManager::OnItemDroppedFromGrid);
	GridWidget->TileSize = InventoryComponent->TileSize;

	// Find the linkage list entry for the component, if none, make a new one
	FInventoryWidgetLink* LinkEntry = GetOrCreateLinkForComponent(InventoryComponent);
	LinkEntry->SetGridWidget(GridWidget);

	// Add to our array
	InventoryWidget->GridWidgets.Add(GridWidget);

	ConfigureGridWidget(InventoryComponent, GridWidget);

	RepositionAllGrids();

	RefreshItemWidgets(InventoryComponent);

	Player->OnInventoryOpened.Broadcast(InventoryComponent);

	return GridWidget;
}

void UInventoryManager::RefreshItemWidgets(UInventoryComponent* InventoryComponent)
{
	if (!InventoryComponent)
	{
		Debug::LogWarning("RefreshInventoryWidgets called with null InventoryComponent", true, 2);
		return;
	}

	if (!InventoryWidget)
	{
		Debug::LogWarning("InventoryWidget is not set in InventoryManager. Please check your setup.", true, 2);
		return;
	}

	// Find the grid widget associated with this inventory component
	FInventoryWidgetLink* LinkEntry = GetOrCreateLinkForComponent(InventoryComponent);
	UInventoryGridWidget* GridWidget = LinkEntry ? LinkEntry->GridWidget : nullptr;
	if (!GridWidget)
	{
		//Debug::LogWarning("No grid widget found for the provided inventory component: " + LinkEntry->InventoryComponent->InventoryName, true, 2);
		return;
	}

	ClearItemWidgets(GridWidget);

	TArray<UItemWidget*> UpdatedWidgets;
	for (const auto& Pair : InventoryComponent->GetAllItems())
	{
		FItemGridPosition ItemPos;
		ItemPos.Item = Pair.Item;
		ItemPos.Position = Pair.Position;

		FInventoryItemPosition ItemPosition = { ItemPos.Item, ItemPos.Position };
		{
			const uint8 StoredRot = InventoryComponent->GetRotationAt(ItemPos.Position);
			ItemPosition.Rotation = InventoryRotationUtils::ReduceRotationForItem(ItemPos.Item, StoredRot);
		}

		const FIntPoint MinOff = InventoryRotationUtils::GetMinRotatedOffset(ItemPos.Item, ItemPosition.Rotation);
		const FVector2D TopLeft(
			(ItemPos.Position.X + MinOff.X) * InventoryComponent->TileSize,
			(ItemPos.Position.Y + MinOff.Y) * InventoryComponent->TileSize
		);

		const bool bIsRecipe = InventoryComponent->IsCraftingStation();

		UItemWidget* TypedItemWidget = CreateItemWidget(GridWidget, ItemPosition, bIsRecipe, InventoryComponent->TileSize);
		if (TypedItemWidget)
		{
			if (UPanelSlot* NewPanelSlot = GridWidget->GetGridCanvasPanel()->AddChild(TypedItemWidget))
			{
				if (UCanvasPanelSlot* CanvasSlot = Cast<UCanvasPanelSlot>(NewPanelSlot))
				{
					// Size explicitly based on rotated dimensions so layout/hit-test match the visual
					const int32 Dx = FMath::Max(1, ItemPos.Item->Dimension.X);
					const int32 Dy = FMath::Max(1, ItemPos.Item->Dimension.Y);
					const bool bOdd = (ItemPosition.Rotation % 2) == 1;
					const FVector2D WidgetSize(
						(bOdd ? Dy : Dx) * InventoryComponent->TileSize,
						(bOdd ? Dx : Dy) * InventoryComponent->TileSize
					);

					CanvasSlot->SetAutoSize(false);
					CanvasSlot->SetSize(WidgetSize);
					CanvasSlot->SetPosition(TopLeft);
					CanvasSlot->SetZOrder(10);
				}
			}

			UpdatedWidgets.Add(TypedItemWidget);
		}
	}

	// Save widgets
	LinkEntry = GetOrCreateLinkForComponent(InventoryComponent);
	if (LinkEntry)
	{
		LinkEntry->SetItemWidgets(UpdatedWidgets);
	}
}

UItemWidget* UInventoryManager::CreateItemWidget(UInventoryGridWidget* GridWidget, FInventoryItemPosition ItemPosition,
                                                 bool bIsRecipe, float TileSize)
{
	UItemWidget* TypedItemWidget = CreateWidget<UItemWidget>(GridWidget, ItemWidgetClass);
	if (!TypedItemWidget)
	{
		return nullptr;
	}

	UInventoryComponent* InventoryComponent = GetComponentWithGridWidget(GridWidget);

	TypedItemWidget->bIsRecipe = bIsRecipe;
	TypedItemWidget->bCanRemoveItem = InventoryComponent->bCanRemoveItems;

	// Uncomment this to disable dragging the item widget
	//TypedItemWidget->bIsDroppable = InventoryComponent->bCanRemoveItems;

	TypedItemWidget->Setup(&ItemPosition, TileSize, InventoryComponent);

	TypedItemWidget->OnMouseEnterDelegate.BindDynamic(this, &UInventoryManager::OnItemEntered);
	TypedItemWidget->OnMouseLeaveDelegate.BindDynamic(this, &UInventoryManager::OnItemExited);
	TypedItemWidget->OnItemDragDetectedDelegate.BindDynamic(this, &UInventoryManager::OnItemDragDetected);
	TypedItemWidget->OnMouseButtonUpDelegate.BindDynamic(this, &UInventoryManager::OnItemClicked);
	TypedItemWidget->OnMouseButtonDoubleClickDelegate.BindDynamic(this, &UInventoryManager::OnItemDoubleClicked);

	return TypedItemWidget;
}

void UInventoryManager::OnItemEntered(UItemWidget* ItemWidget, UInventoryItem* Item)
{
	// Process for when the cusor enter item widget area
}

void UInventoryManager::OnItemExited()
{
	// Process for when the cursor exit item widget area
}

void UInventoryManager::OnItemDragDetected(UItemWidget* ItemWidget, FInventoryItemPosition InventoryItemPosition)
{
	Process for when an drag operation is detected
}

void UInventoryManager::OnItemClicked(UItemWidget* ItemWidget, FInventoryItemPosition InventoryItemPosition,
                                      bool bIsShiftClicked)
{
	// Process for when an item widget is clicked
}

void UInventoryManager::OnItemDoubleClicked(UItemWidget* ItemWidget, FInventoryItemPosition InventoryItemPosition)
{
	// Process for when an item widget is double clicked
}

void UInventoryManager::OnItemDroppedFromGrid(UInventoryGridWidget* GridWidget,
                                              FInventoryItemPosition InventoryItemPosition)
{
	// Process for when drag operation ended with the dragged item being dropped on a grid widget
}

void UInventoryManager::OnItemDroppedFromInventory(UInventoryWidget* InInventoryWidget, UInventoryItem* InventoryItem)
{
	// Process for when drag operation ended with the dragged item being dropped on the inventory widget
}
Half way through the project, we had some difficulties working with the inventory system: All the logic are separated in different UI elements which makes it very difficult to pinpoint where might be causing issues and bugs; The InventoryComponents stored a direct reference to the WorldItem, but when an item is picked up, the WorldItemis also destroying in the process. This caused some unpredictable behavior such as inventory wipe and crashes. For the refactor, I suggested two changes: The first being that we should use the mediator pattern to centralize all the logic into one script, and to make sure communication between the inventory component and the widgets all happen in one location as well; The second change is that instead of storing the WorldItem in the InventoryComponent, we store a pair of DataAsset to grid position, this way, we don't need to keep the WorldItem alive all the time.