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

@@ -72,7 +72,7 @@ r.TranslucentSortPolicy=0
TranslucentSortAxis=(X=0.000000,Y=-1.000000,Z=0.000000)
r.CustomDepth=3
r.CustomDepthTemporalAAJitter=False
r.PostProcessing.PropagateAlpha=0
r.PostProcessing.PropagateAlpha=False
r.DefaultFeature.Bloom=True
r.DefaultFeature.AmbientOcclusion=True
r.DefaultFeature.AmbientOcclusionStaticFraction=False
@@ -198,6 +198,7 @@ r.DistanceFieldShadowing=0
r.AmbientOcclusionLevels=1
r.Shadow.CSMCaching=0
r.DOF.Recombine.Quality=1
r.Deferred.SupportPrimitiveAlphaHoldout=False
[/Script/LinuxTargetPlatform.LinuxTargetSettings]
TargetArchitecture=X86_64UnknownLinuxGnu

BIN
Content/Input/IA_ToggleMap.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Input/UI/IAUI_Zoom.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/UI/WBP_MapWidget.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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)
{
@@ -223,16 +228,22 @@ void AEleriPlayerController::SetupInputComponent()
{
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;
};