Finish map functionality, progress on base character template, tweak Eleri

This commit is contained in:
2026-09-25 22:39:30 +02:00
parent aa4c87d88a
commit d173cf8b1e
97 changed files with 2150 additions and 167 deletions

View File

@@ -12,7 +12,7 @@ void UDialogueWidget::NativeOnInitialized()
EleriPlayerController = Cast<AEleriPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
}
void UDialogueWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction)
void UDialogueWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value)
{
UE_LOG(LogTemp, Warning, TEXT(""));
}

View File

@@ -29,7 +29,7 @@ public:
bool IsTypingDialogue() const { return bIsTypingDialogue; }
virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction) override;
virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) override;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite)

View File

@@ -14,6 +14,7 @@
#include "TimerManager.h"
#include "ProjectEleri/UI/CalendarScreen.h"
#include "ProjectEleri/UI/InteractionWidget.h"
#include "ProjectEleri/UI/MapWidget.h"
#include "UI/AlchemyWidget.h"
#include "UI/EleriBaseWidget.h"
#include "ProjectEleri/GameObjects/BookActor.h"
@@ -204,6 +205,10 @@ void AEleriPlayerController::SetupInputComponent()
{
PlayerEnhancedInputComponent->BindAction(CalendarAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnCalendarToggle);
}
if (MapAction)
{
PlayerEnhancedInputComponent->BindAction(MapAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnMapToggle);
}
if (InteractAction)
{
@@ -218,21 +223,27 @@ void AEleriPlayerController::SetupInputComponent()
{
PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed);
}
if (UiBackAction)
{
PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction);
}
if (UiTabLeftAction)
{
PlayerEnhancedInputComponent->BindAction(UiTabLeftAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerTabLeftAction);
}
if (UiTabRightAction)
{
PlayerEnhancedInputComponent->BindAction(UiTabRightAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerTabRightAction);
}
if (UiZoomAction)
{
PlayerEnhancedInputComponent->BindAction(UiZoomAction, ETriggerEvent::Triggered, this, &AEleriPlayerController::OnControllerZoomAction);
}
if (UiMovePointerAction)
{
PlayerEnhancedInputComponent->BindAction(UiMovePointerAction, ETriggerEvent::Triggered, this, &AEleriPlayerController::OnControllerMovePointerAction);
}
//---------------------
// OBJECT REMOVAL INPUTS
@@ -814,7 +825,7 @@ void AEleriPlayerController::OnControllerConfirmAction(const FInputActionValue &
return;
}
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Select);
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Select, Value);
}
void AEleriPlayerController::OnControllerBackAction(const FInputActionValue &Value)
@@ -825,7 +836,7 @@ void AEleriPlayerController::OnControllerBackAction(const FInputActionValue &Val
return;
}
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Back);
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Back, Value);
}
void AEleriPlayerController::OnControllerTabLeftAction(const FInputActionValue &Value)
@@ -835,7 +846,7 @@ void AEleriPlayerController::OnControllerTabLeftAction(const FInputActionValue &
return;
}
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabLeft);
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabLeft, Value);
}
void AEleriPlayerController::OnControllerTabRightAction(const FInputActionValue &Value)
@@ -845,7 +856,27 @@ void AEleriPlayerController::OnControllerTabRightAction(const FInputActionValue
return;
}
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabRight);
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabRight, Value);
}
void AEleriPlayerController::OnControllerZoomAction(const FInputActionValue& Value)
{
if (MainGameUI->GetWidgetStack().IsEmpty())
{
return;
}
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Zoom, Value);
}
void AEleriPlayerController::OnControllerMovePointerAction(const FInputActionValue& Value)
{
if (MainGameUI->GetWidgetStack().IsEmpty())
{
return;
}
MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::MovePointer, Value);
}
// UI BIND
@@ -904,6 +935,14 @@ void AEleriPlayerController::OnCalendarToggle(const FInputActionValue &Value)
OpenCalendarWindow();
}
void AEleriPlayerController::OnMapToggle(const FInputActionValue& Value)
{
if (Paused || IsDialogueOpen() || (IsInMenu() && !MainGameUI->IsCalendarOpen()) || !MoveActorsComp || MoveActorsComp->IsRemovingMode())
return;
OpenMapWindow();
}
void AEleriPlayerController::OnBookNextPage(const FInputActionValue &Value)
{
if (!IsValid(BookOfAlchemyManager))
@@ -1132,6 +1171,12 @@ UCalendarScreen* AEleriPlayerController::OpenCalendarWindow()
return MainGameUI->GetCalendarWidget();
}
UEleriBaseWidget* AEleriPlayerController::OpenMapWindow()
{
MainGameUI->OpenWidget(MainGameUI->GetMapWidget());
return MainGameUI->GetMapWidget();
}
UEleriBaseWidget* AEleriPlayerController::OpenBazarWindow() {
MainGameUI->OpenWidget(MainGameUI->GetBazarWidget());
return MainGameUI->GetBazarWidget();

View File

@@ -12,7 +12,7 @@ void UEleriBaseWidget::ToggleWidget_Implementation(bool bActive) {
SetVisibility(bActive? ESlateVisibility::Visible : ESlateVisibility::Hidden);
}
void UEleriBaseWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction)
void UEleriBaseWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value)
{
if (InputAction == EEleriUIInputAction::Back)
{

View File

@@ -154,6 +154,11 @@ bool UMainGameWidget::IsBazarWidgetOpen() const
return WidgetStack.Contains(Bazar_Widget);
}
bool UMainGameWidget::IsMapWidgetOpen() const
{
return WidgetStack.Contains(Map_Widget);
}
bool UMainGameWidget::IsScreenActionInputDisplayWidgetOpen() const
{
return ScreenActionInputDisplay_Widget->GetVisibility() == ESlateVisibility::Visible;

View File

@@ -17,6 +17,7 @@
#include "EleriPlayerController.generated.h"
class UMapWidget;
class ASeedBedActor;
class UItemDataAsset;
@@ -197,6 +198,8 @@ public:
UInputAction* OrdersAction;
UPROPERTY(EditDefaultsOnly, Category = "UI Input")
UInputAction* CalendarAction;
UPROPERTY(EditDefaultsOnly, Category = "UI Input")
UInputAction* MapAction;
// UI INPUT ACTIONS
UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad")
@@ -218,6 +221,10 @@ public:
UInputAction* UiTabLeftAction;
UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad")
UInputAction* UiTabRightAction;
UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad")
UInputAction* UiZoomAction;
UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad")
UInputAction* UiMovePointerAction;
UPROPERTY(BlueprintAssignable)
FOnUiButtonPressed OnUiButtonPressed;
@@ -318,12 +325,15 @@ public:
void OnControllerBackAction(const FInputActionValue& Value);
void OnControllerTabLeftAction(const FInputActionValue& Value);
void OnControllerTabRightAction(const FInputActionValue& Value);
void OnControllerZoomAction(const FInputActionValue& Value);
void OnControllerMovePointerAction(const FInputActionValue& Value);
void OnMainMenuToggle(const FInputActionValue& Value);
void OnInventoryToggle(const FInputActionValue& Value);
void OnBookToggle(const FInputActionValue& Value);
void OnOrdersToggle(const FInputActionValue& Value);
void OnCalendarToggle(const FInputActionValue& Value);
void OnMapToggle(const FInputActionValue& Value);
void OnBookNextPage(const FInputActionValue& Value);
void OnBookPreviousPage(const FInputActionValue& Value);
@@ -366,6 +376,8 @@ public:
UFUNCTION(BlueprintCallable)
UCalendarScreen* OpenCalendarWindow();
UFUNCTION(BlueprintCallable)
UEleriBaseWidget* OpenMapWindow();
// Bazar
UFUNCTION(BlueprintCallable)

View File

@@ -15,7 +15,9 @@ enum class EEleriUIInputAction : uint8
Select,
Back,
TabLeft,
TabRight
TabRight,
Zoom,
MovePointer
};
USTRUCT(BlueprintType)
@@ -67,8 +69,8 @@ public:
virtual void ToggleWidget_Implementation(bool bActive);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
void HandleUiInputAction(EEleriUIInputAction InputAction);
virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction);
void HandleUiInputAction(EEleriUIInputAction InputAction, FInputActionValue Value);
virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value);
UFUNCTION(BlueprintCallable, Category = "Eleri|UI")
void DisplayInputPrompts(const FName PromptMapKey = "Default");

View File

@@ -150,6 +150,12 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure)
bool IsBazarWidgetOpen() const;
// Map widget
UFUNCTION(BlueprintCallable, BlueprintPure)
UEleriBaseWidget* GetMapWidget() { return Map_Widget; }
UFUNCTION(BlueprintCallable, BlueprintPure)
bool IsMapWidgetOpen() const;
UFUNCTION(BlueprintCallable, BlueprintPure)
UScreenActionInputDisplay* GetScreenActionInputDisplayWidget() { return ScreenActionInputDisplay_Widget; }
UFUNCTION(BlueprintCallable, BlueprintPure)
@@ -201,10 +207,14 @@ protected:
UPROPERTY(meta = (BindWidget))
UCalendarScreen *Calendar_Widget;
// Calendar
// Bazaar
UPROPERTY(meta = (BindWidget))
UEleriBaseWidget* Bazar_Widget;
// Bazaar
UPROPERTY(meta = (BindWidget))
UEleriBaseWidget* Map_Widget;
UPROPERTY(meta = (BindWidget))
UScreenActionInputDisplay* ScreenActionInputDisplay_Widget;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,535 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UI/EleriBaseWidget.h"
#include "MapWidget.generated.h"
class AOrthoMapCaptureActor;
class UBorder;
class UCanvasPanel;
class UImage;
class UOverlay;
class UTexture2D;
class UWidget;
/** What a request for a set of assets to be loaded is held by. Only used through a shared pointer here, so the
* full declaration is left to the file that actually asks for the pieces. */
struct FStreamableHandle;
/** How the widget decides which piece asset belongs in which cell of the map. */
UENUM(BlueprintType)
enum class EMapPieceLayout : uint8
{
/** Ask the capture actor's own world to map mapping where each piece's world box lands. This is the
* default because it is the same mapping the player marker is placed through, so the picture and the
* marker cannot describe two different maps. Falls back to the filename order when there is no actor
* to ask, or when the answer is not a lattice. */
SampledFromMapping UMETA(DisplayName = "Sampled From The Mapping"),
/** Put the piece whose filename says column C, row R in the cell C across and R down, and ask nothing.
* What is left when the mapping cannot be consulted. */
FilenameColumnRow UMETA(DisplayName = "Filename Column Row")
};
/** Whether each piece's own picture is mirrored inside its cell. The actor's bFlipU and bFlipV mirror the
* whole map, and a piece baked as a map of its own carries that mirroring either inside the asset or not
* at all, depending on how the bake wrote it. Nothing the widget can ask, so it is a setting, and off is
* right for the usual bake: the one that reads the way the world does. */
UENUM(BlueprintType)
enum class EMapPieceMirroring : uint8
{
/** Draw the piece as the asset has it. */
None UMETA(DisplayName = "As The Asset Has It"),
/** Mirror every piece left to right, the counterpart of the actor's bFlipU. */
Horizontal UMETA(DisplayName = "Horizontal"),
/** Mirror every piece top to bottom, the counterpart of the actor's bFlipV. */
Vertical UMETA(DisplayName = "Vertical"),
/** Both, for a bake that was made with both flags on. */
Both UMETA(DisplayName = "Both")
};
/** Which way the player is said to be facing, which is the direction the marker is turned to point along. A map
* is usually asked which way the player is going, and a pawn and a camera can disagree about that: this is the
* setting that says which of them the map should believe. */
UENUM(BlueprintType)
enum class EMapMarkerFacing : uint8
{
/** The way the pawn is turned to, which is what the player's own forward is for everything that moves
* them. The default, because the pawn is what the marker's place on the map comes from. */
PawnRotation UMETA(DisplayName = "Pawn Rotation"),
/** The way the controller is looking, which is the camera's own yaw and can be a long way from the pawn's
* while the pawn is moving sideways or backwards. */
ControlRotation UMETA(DisplayName = "Control Rotation"),
/** The way the pawn is moving. A pawn standing still points nowhere by its velocity, so the way it is
* turned to stands in. */
Velocity UMETA(DisplayName = "Velocity"),
/** Not the player's facing at all: the marker is turned by MarkerFacingOffset and nothing else, which is
* what a marker that should sit still, or art that has to be squared up, is for. */
None UMETA(DisplayName = "Not Turned To The Player")
};
/** One cell of the map: which piece of the bake was asked for it, and where the widget put it. Kept so that
* ReportMapLayout can say what the widget decided without the decision being worked out again. */
USTRUCT(BlueprintType)
struct FEleriMapPieceCell
{
GENERATED_BODY()
/** Column in the piece's own filename. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
int32 SourceColumn = 0;
/** Row in the piece's own filename. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
int32 SourceRow = 0;
/** Column the piece is drawn in, counted across from the left of the map. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
int32 CellColumn = 0;
/** Row the piece is drawn in, counted down from the top of the map. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
int32 CellRow = 0;
/** The asset this cell is showing, as a long object path. Reported even when the load fails, because a
* path that does not exist is the first thing worth knowing about a cell that came back empty. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
FString ObjectPath;
/** The texture the cell is showing, or null while its piece is still arriving, which is the normal state
* for a moment after the map is first opened. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
TObjectPtr<UTexture2D> Texture = nullptr;
};
/**
* The world map, as a grid of the baked map's pieces.
*
* The bake writes one texture per piece, each a map of its own region, and this widget lays them out in a
* grid, one cell per piece, with the player marked where their world position falls. Nothing here is drawn
* from the level at runtime: the pieces are the bake, and the level only says where a world position lands
* in them, which is what the capture actor's world to map mapping is for.
*
* The map itself is built in code, in RebuildWidget, because a user widget's slate widget is made from the root
* of its widget tree and there is no tree at all until the widget has been initialized. A blueprint subclass of
* this therefore needs to hold nothing: it may be empty, or its root may be a panel, which is where this
* widget's own tree is added. The one part of the map that is not built here is the player's marker, which is
* the widget named PlayerMarker in that blueprint: what a marker looks like is the design's business, and only
* where it sits on the map is the map's.
*
* The pieces are 16384 pixel virtual textures, so loading all 64 of them eagerly would be a stall and a
* virtual texture pool full of a map nobody is looking at. They are asked for lazily, the first time the
* widget is shown, in one batched asynchronous load, and the renderer decides from there what of them is
* resident.
*/
UCLASS()
class PROJECTELERI_API UMapWidget : public UEleriBaseWidget
{
GENERATED_BODY()
public:
UMapWidget();
//----- Map source -----
/** Map the pieces were baked from. Spelled out rather than taken from the world, because in PIE the
* world's own name is prefixed (UEDPIE_0_Persistent_GameWorld) and the assets are not. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
FString MapName;
/** Folder the baked map was saved into, as a long package path: the same one the capture actor's
* OutputPackageFolder names. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
FString PiecePackageFolder;
/** Name of one piece, with {Map}, {Grid}, {Col} and {Row} standing in for the parts that vary. The
* pieces are named after the map they came from, so a map and its pieces can be re-baked without this
* having to be edited. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
FString PieceAssetPattern;
/** Pieces per axis: the grid the map was cut into, and the side of the grid that is drawn. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source", meta = (ClampMin = "1", ClampMax = "64"))
int32 GridSize;
/** How the widget works out which piece goes where. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
EMapPieceLayout PieceLayout;
/** Whether each piece is mirrored inside its cell. Only a map baked with the capture actor's bFlipU or
* bFlipV needs anything other than the default, and the widget warns at load when one of those flags is
* on while this still says the pieces are drawn as the assets have them. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
EMapPieceMirroring PieceMirroring;
/** Whether the pieces are asked for as soon as the widget is constructed rather than the first time it
* is shown. Off is the lazy default: the map is not usually the first thing a player opens. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Loading")
bool bPreloadPiecesOnConstruct;
//----- View settings -----
/** Zoom of one fits the whole map across the shorter side of the view, so the map is as large as it can
* be and still be seen whole. Everything else is relative to that. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01"))
float DefaultZoom;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01"))
float MinZoom;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01"))
float MaxZoom;
/** How much one step of zoom changes the zoom: one notch of the wheel, and one call to ZoomIn or
* ZoomOut. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "1.0001"))
float ZoomStep;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "1.0001"))
float MoveStep;
/** Design units one piece is drawn at, which fixes how large the whole map is before zoom is applied.
* It sets the size the pieces are laid out at, not how large they appear: zoom one still fits the map
* to the view. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "16"))
float PieceCellSize;
/** Whether opening the map starts with the whole map in view and centred on the player, which is what
* the map is for. Off leaves whatever the player had zoomed and panned to. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View")
bool bResetViewWhenShown;
//----- Marker settings -----
/** Whether the marker follows the player as they move. Off freezes it where it was, which is what a
* player comparing two places wants. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker")
bool bFollowPlayerByDefault;
/** Whether the view keeps the player at its centre as they move. Dragging the map turns this off, so
* that looking somewhere else is not fought by the marker being pulled back every frame. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker")
bool bCenterOnPlayerByDefault;
/** The player's place on the map: the widget of this name in the blueprint this map is drawn in, found by
* name rather than made here. Nothing in this file draws it, so what the marker looks like -- its brush,
* its colours, its corner radii -- belongs to the design. The map takes it onto its own layer, which is
* what a marker has to be on to move and zoom with the map it marks, and then sizes it, places it and
* turns it. Any kind of widget will do, from an image to a panel holding several. Optional, because a
* design with no marker in it is a map that says so once rather than a map that will not open. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker", meta = (BindWidgetOptional))
TObjectPtr<UWidget> PlayerMarker;
/** How long the marker's longer side is drawn, in the map's own design units, which is the one thing about
* the marker's look the map has to know, because the map is what places it and is measured in these units.
* What shape it is comes from the design: an icon's shape is the shape of its art, and anything else is
* drawn at the shape it asks to be, so a wide or a tall marker is not squeezed into a square. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker", meta = (ClampMin = "2"))
float MarkerSize;
/** Degrees added to the turn the marker is given, for art that does not point up: an arrow drawn pointing
* right wants -90 here, and one drawn pointing down wants 180. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker")
float MarkerFacingOffset;
/** Colour the map's own rectangle is filled with, so that a piece still arriving reads as a part of the
* map that has not been drawn yet rather than as nothing at all. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View")
FLinearColor MapBackgroundColor;
//----- View state -----
/** Zoom relative to fitting the map in the view, held between MinZoom and MaxZoom. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|View")
float Zoom;
/** Whether the marker is following the player. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker")
bool bFollowPlayer;
/** Whether the view is keeping the player at its centre. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker")
bool bCenterOnPlayer;
/** Whether the map is being dragged right now, which is worth knowing outside the widget because a drag
* is not a click on whatever is behind the map. */
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|View")
bool bPanning;
//----- View API -----
/** Sets the zoom, held between MinZoom and MaxZoom. One is the whole map in view. */
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View")
void SetZoom(float NewZoom);
/** Zooms by a signed number of steps: positive zooms in, negative zooms out, 0 does nothing.
* A magnitude of 1 is one full ZoomStep, so this can be fed the raw value from a wheel or
* gamepad trigger axis directly. Fractional or >1 magnitudes scale accordingly. */
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View")
void ZoomBy(float Amount);
/** Fits the whole map in the view, centres it on the player, and holds it there again. */
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View")
void ResetView();
//----- Marker API -----
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Marker")
void SetFollowPlayer(bool bNewFollow);
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Marker")
bool IsFollowingPlayer() const { return bFollowPlayer; }
/** Whether the view is centred on the player, which it is until the map is dragged. */
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Marker")
void SetCenterOnPlayer(bool bNewCenter);
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Marker")
bool IsCenteringOnPlayer() const { return bCenterOnPlayer; }
//----- The bake this widget is showing -----
/** What the widget has worked out about the bake so far: which piece the pattern asks for, where it was
* put, and whether it has arrived. */
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Source")
const TArray<FEleriMapPieceCell>& GetPieceCells() const { return PieceCells; }
/** The capture actor the region and the mapping came from, or null when there is none in the world. Null
* means the player cannot be placed and the lattice could not be checked, which is reported at load
* rather than treated as an error. */
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Source")
AOrthoMapCaptureActor* GetMapCaptureActor() const { return CaptureActor; }
/** Does the load again: works the layout out from scratch and asks for the pieces, whether they were
* asked for before or not. The thing to call after re-baking the map without leaving the editor. */
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Source")
void RefreshPieces();
/** Logs the bake this widget is showing: the pattern and folder it asked, whether the mapping could be
* consulted, how many pieces arrived, and every cell's piece and place. The thing to look at when the
* map is blank, or whole but wrong. */
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Source")
void ReportMapLayout();
/** Everything the widget does when it is shown: resolves the bake, asks for the pieces the first time,
* and fits the map to the view. Exposed so that a blueprint can do the same work for a show this widget
* did not get to see, and so that the show path is one thing rather than three. */
UFUNCTION(BlueprintCallable, Category = "Eleri|Map")
void HandleShown();
protected:
//----- Lifetime, in the form the engine hands it over -----
virtual void ToggleWidget_Implementation(bool bActive) override;
virtual void SetVisibility(ESlateVisibility InVisibility) override;
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
/** Where the tree has to be built: a user widget's slate widget is made by taking the root of its widget
* tree, and there is no tree until the widget has been initialized, which this call makes sure of. */
virtual TSharedRef<SWidget> RebuildWidget() override;
//----- Input, in the form the engine hands it over -----
virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) override;
//----- Building and loading -----
/** Builds the widget tree if it is not there already, so that calling this twice is nothing. */
void EnsureWidgetTree();
/** Resolves the bake and asks for its pieces, once. Called on the way to being shown rather than at
* construction, so that the world is up and the map is only paid for when it is wanted. Doing it twice
* is nothing. */
void EnsureMapReady();
/** Works out which piece belongs in which cell and fills PieceCells with it. Reports the reason and
* returns false when the mapping could not be consulted, which leaves the caller to say so. */
bool ResolveMapLayout();
/** Works out how the baked map lies in the world: what a step of one world unit along X does to the map's
* own UV, and what a step along Y does, both taken from the capture actor's own mapping. That is what the
* marker is turned by, and taking it from the mapping is what makes a bake that was captured turned, or
* mirrored with the actor's flip flags, come out right without anything being assumed about it. */
void ResolveMapAxes();
/** The mapping route to a lattice: every piece's world box centre put through the capture actor's own
* world to map mapping, which is the mapping the marker is placed through as well. False with a reason
* when there is no actor, when a piece lands off the map, or when two pieces land in one cell. */
bool TrySamplePieceCells(FText& OutReason);
/** Where a piece's own cell of the region lands in the baked map, as a cell of the grid. */
bool GetPieceCellFromMapping(int32 SourceColumn, int32 SourceRow, FIntPoint& OutCell) const;
/** The other route: the piece whose filename says column C, row R goes in the cell C across and R down,
* without asking anything. */
void FillPieceCellsFromFilenames();
/** One piece's box in the world, from the region the pieces were cut out of: columns run along X and
* rows along Y from its min corner, the way round the bake's own piece boxes run. */
FBox GetPieceWorldBox(int32 SourceColumn, int32 SourceRow) const;
/** Asks for every piece in PieceCells in one batched asynchronous load. */
void RequestPieceLoads();
/** Puts every arrived piece on its image, and reports the ones that did not arrive. */
void ApplyLoadedPieces();
/** The brush one piece image is drawn with: the piece's own texture, and the mirroring the bake needs,
* which is the one thing about a piece that cannot be asked for and has to be told. */
void ApplyPieceBrush(UImage* PieceImage, UTexture2D* PieceTexture) const;
//----- View -----
/** Grid side of the drawn map in design units: GridSize pieces of PieceCellSize. */
float GetMapDesignSize() const;
/** Side of the view the map is fit into, in slate units, or zero before the widget has been laid out. */
FVector2D GetViewSize() const;
/** Slate units one design unit of the map is drawn at: the fit-relative zoom, or zero before the widget
* has been laid out. Everything that turns a screen distance into a map distance goes through it. */
float GetMapScale() const;
/** The content point held at the centre of the view, clamped to the map so that it cannot be dragged
* off into the void around it. */
FVector2D GetClampedFocusPoint(const FVector2D& InFocusPoint) const;
/** Puts a content position at the centre of the view, clamped. */
void FocusOnContent(const FVector2D& ContentPosition);
/** Hands the zoom and the focus point to the map layer, and does nothing when they have not changed, so
* that following the player does not invalidate a transform that is already right. */
void ApplyViewTransform();
/** Where the player is in the baked map, or false when there is nothing to say: no pawn, or no capture
* actor to put their position through. */
bool GetPlayerMapUV(FVector2D& OutUV) const;
/** The size the marker is drawn at: the shape the design gave it, with its longer side at MarkerSize, so
* that a wide or a tall marker is not squeezed into a square. A marker with nothing to say what shape it
* is -- a box with nothing in it, or a panel with nothing in that -- is drawn square. */
FVector2D GetMarkerSlotSize() const;
/** The angle the marker is turned to on screen, in degrees clockwise, or false when there is nothing to
* say: a mapping that cannot say how the map lies in the world, or a facing that cannot be had. */
bool ResolveMarkerAngle(float& OutAngle) const;
/** Puts the marker where the player is, and hides it when they are off the map. */
void UpdateMarkerView();
//----- Drag and wheel -----
/** Starts a drag, from the screen position it started at. One pointer at a time: a second finger while a
* drag is running is ignored rather than added, because the map is dragged and not pinched. */
void BeginPan(int32 PointerIndex, const FVector2D& ScreenPosition);
/** Moves the map under the pointer, which is also what turns the view loose from the player. */
void UpdatePan(const FVector2D& Delta);
/** Ends the drag this pointer started, and does nothing for any other pointer. */
void EndPan(int32 PointerIndex);
//----- Widgets the map builds -----
/** Root of the tree this widget builds. */
UPROPERTY(Transient)
TObjectPtr<UOverlay> MapRoot;
/** What the map is looked at through: everything outside it is clipped, and it is what the map is fit
* into. */
UPROPERTY(Transient)
TObjectPtr<UOverlay> MapViewport;
/** What the map is drawn on: the map's own rectangle, the pieces, and the marker, moved and scaled by
* one transform rather than 64. */
UPROPERTY(Transient)
TObjectPtr<UCanvasPanel> MapLayer;
/** The region the map covers, drawn so that a piece still arriving is a pale part of the map rather than
* a hole in it. */
UPROPERTY(Transient)
TObjectPtr<UBorder> MapBackground;
/** The 64 pieces, each at the cell the layout put it in. */
UPROPERTY(Transient)
TObjectPtr<UCanvasPanel> PieceGrid;
/** One image per piece, in cell order: row by row from the top left. */
UPROPERTY(Transient)
TArray<TObjectPtr<UImage>> PieceImages;
/** Takes every pointer event the map needs, so that a drag is never also a click on the level behind
* the map. */
UPROPERTY(Transient)
TObjectPtr<UBorder> Interaction;
//----- State -----
/** The bake as the widget worked it out: one entry per cell, whether the piece has arrived or not. */
UPROPERTY(Transient)
TArray<FEleriMapPieceCell> PieceCells;
/** The capture actor the region and the mapping came from: cached because it is asked a great deal, and
* found again whenever it has gone away. */
UPROPERTY(Transient)
TObjectPtr<AOrthoMapCaptureActor> CaptureActor;
/** Whether the layout and the piece requests have been worked out. False means a later call still has
* work to do, which is what happens when the map is shown before the world has its capture actor. */
bool bMapReady;
/** Whether the pieces have been asked for, so that asking happens once rather than once per show. */
bool bPiecesRequested;
/** What does the loading, held so that the pieces stay wanted until they have arrived. */
TSharedPtr<FStreamableHandle> PieceStreamHandle;
/** How much the map is zoomed, and the content point held at the centre of the view. */
FVector2D FocusPoint;
/** Where the player is on the map, as the map's own UV, and whether the marker has ever been placed.
* Held rather than asked for every frame so that the marker can be frozen where the player was, which
* is what turning following off is for. */
FVector2D MarkerMapUV;
bool bMarkerPlaced;
/** The angle the marker is turned to on screen, in degrees clockwise, and whether it has been written yet.
* Held so that a tick which leaves the facing alone writes nothing. */
float MarkerAngle;
bool bMarkerAngleApplied;
/** Whether the map has already said that the design has no marker in it, so that a map drawn without one
* says so once rather than on every update. */
bool bMissingMarkerReported;
/** How the baked map's own UV runs through the world: what a step of one world unit along X moves it, and
* what a step along Y moves it. Both are taken from the capture actor's own mapping, and both being zero
* is what bMapAxesResolved says no to. */
FVector2D MapUVPerWorldX;
FVector2D MapUVPerWorldY;
/** Whether the two above have been worked out: false means the marker is placed but not turned. */
bool bMapAxesResolved;
/** The transform last handed to the map layer, so that a tick which changes nothing writes nothing. */
FWidgetTransform AppliedMapTransform;
/** Whether the transform above is worth comparing against yet. */
bool bMapTransformApplied;
/** Which pointer is dragging, or INDEX_NONE. */
int32 PanPointerIndex;
/** Where the pointer was when the drag last moved, in screen space. */
FVector2D LastPanScreenPosition;
};