#pragma once #include "CoreMinimal.h" #include "Components/WidgetComponent.h" #include "GameFramework/Actor.h" #include "UI/ViewModels/CargoViewModel.h" #include "UI/ViewModels/DashViewModel.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SLeafWidget.h" #include "DescentDash.generated.h" /** * The vehicle dashboards (DESIGN_UE.md 4.6, requirements V6 and U11) as Slate widgets driven by plain view-models, usable on * screen and on a UWidgetComponent in the world. * * SDescentDash 1024 x 420: hull and power as clean 270 degree arcs (no needles), depth in 86 px type, rating or crush * warning, depth bar with a red rating tick, temperature and speed, hold and biotank line. * SCargoDisplay 512 x 512: the hold as a picture. A cut-away tank of cells filled bottom-up in arrival order, a faceted * chunk in its ore colour with its symbol in each, header CARGO n/cap (HOLD FULL in red with a pulsing outline), * a footer of three ore chips and a vertical green bar for the biotank. A new chunk drops in over 0.3 s. */ class THEDESCENT_API SDescentDash : public SLeafWidget { public: SLATE_BEGIN_ARGS(SDescentDash) {} SLATE_ARGUMENT(TSharedPtr, Model) SLATE_END_ARGS() void Construct(const FArguments& InArgs); virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; virtual FVector2D ComputeDesiredSize(float) const override; /** The text and fractions the picture shows, as plain values (so tests check them without a renderer). */ struct FReadout { FString DepthText; // "2,061 m" FString RatingText; // "RATING 2,300 m" or "!! CRUSH DEPTH 2,300 m" bool bCrush = false; FString TitleText; // "BT-01 TENDER - DRILL" FString Line1; // "TEMP 5.1 C SPD 1.8 m/s" FString Line2; // "HOLD 5/8 - BIO 1.2/4.0" bool bHoldFull = false; float DepthBar = 0.f; // 0..1 over 1,740..3,400 m float RatingTick = 0.f; // 0..1 position of the red tick float Hull01 = 0.f; float Power01 = 0.f; bool bHullRed = false; bool bPowerRed = false; }; static FReadout MakeReadout(const FTenderDashModel& M); private: TSharedPtr Model; }; class THEDESCENT_API SCargoDisplay : public SLeafWidget { public: SLATE_BEGIN_ARGS(SCargoDisplay) {} SLATE_ARGUMENT(TSharedPtr, Model) SLATE_END_ARGS() void Construct(const FArguments& InArgs); virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; virtual FVector2D ComputeDesiredSize(float) const override; /** True while something moves (a chunk dropping in, the full-hold pulse): the owner should redraw every frame then, not at 8 Hz. */ bool IsAnimating(double NowSeconds) const; /** Where a cell of a hold of `Cap` cells sits in a tank area, and its size. Cell 0 is bottom-left; rows grow upward. */ struct FCellLayout { float Cell = 0.f; FVector2f Origin = FVector2f::ZeroVector; // top-left of cell 0's row/col grid inside the tank int32 Rows = 0; }; static FCellLayout LayoutCells(int32 Cap, const FVector2f& TankSize); static FVector2f CellPosition(const FCellLayout& L, int32 Index, const FVector2f& TankSize); private: TSharedPtr Model; mutable int32 LastUsed = -1; mutable int32 DropIndex = -1; mutable double DropStart = -100.0; }; enum class EDescentDashKind : uint8 { Main, Cargo }; /** * A UWidgetComponent that hosts a dashboard widget in the world: manual redraw at 8 Hz (section 1.4, dashboards), at once when * the rounded depth changes, and every frame while the cargo display animates. Unlit, opaque: it is lit only by itself. * Create it with NewObject, attach, RegisterComponent, then Setup(). */ UCLASS(ClassGroup = (Descent), meta = (BlueprintSpawnableComponent)) class THEDESCENT_API UDescentDashComponent : public UWidgetComponent { GENERATED_BODY() public: UDescentDashComponent(); void Setup(EDescentDashKind InKind); /** Sizes the quad so that it is WidthM metres wide (the height follows the draw size). */ void SetWorldWidthM(float WidthM); void SetDashModel(const FTenderDashModel& Model); void SetCargoModel(const FCargoDisplayModel& Model); const FTenderDashModel* GetDashModel() const { return DashModel.Get(); } const FCargoDisplayModel* GetCargoModel() const { return CargoModel.Get(); } EDescentDashKind GetKind() const { return Kind; } int32 RedrawCount() const { return Redraws; } virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; private: EDescentDashKind Kind = EDescentDashKind::Main; TSharedPtr DashModel; TSharedPtr CargoModel; TSharedPtr CargoWidget; float SinceRedraw = 0.f; FString LastDepthText; int32 Redraws = 0; }; /** A free-standing dashboard panel in the world (scenarios, and the template the Tender's cockpit uses for DashMain / DashCargo). */ UCLASS() class THEDESCENT_API ADescentDashPanel : public AActor { GENERATED_BODY() public: ADescentDashPanel(); static ADescentDashPanel* Spawn(UWorld* World, const FTransform& Transform, EDescentDashKind Kind, float WidthM); UPROPERTY(VisibleAnywhere) TObjectPtr Dash; };