#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Core/DescentHandle.h" #include "Core/DescentWorldSubsystem.h" #include "Core/Tuning/DescentTune_Cargo.h" #include "Player/DescentInteraction.h" #include "Systems/DescentBuild.h" #include "DescentCargo.generated.h" class AKazahTender; class ADescentCarrier; class ADescentCharacter; class ADescentBoxRack; class ADescentRobotArm; class UInstancedStaticMeshComponent; class UStaticMeshComponent; class UTextRenderComponent; /** The pure parts of the cargo loop (M5), testable without a world. Ore types: 0 ferrite, 1 cobalt, 2 pearl. */ namespace Descent::Cargo { THEDESCENT_API FName OreName(uint8 Type); THEDESCENT_API FName RefinedName(uint8 Type); // ingot, wafer, lattice THEDESCENT_API int32 OreTypeOf(FName Ore); // -1 if not an ore THEDESCENT_API int32 RefinedTypeOf(FName Item); // -1 if not a refined good /** Section 6.3: the ore pipe from the belly coupling of the Tender at BOARD to the packer inlet, carrier-local metres. */ THEDESCENT_API const TArray& OrePipeRouteM(); THEDESCENT_API double RouteLengthM(const TArray& Route); THEDESCENT_API FVector RoutePointM(const TArray& Route, double Dist); /** Section 6.2 ConsumeOre: takes N pieces from box counts, the emptiest box first. Returns what was taken; emptied boxes hold 0. */ THEDESCENT_API int32 ConsumeFromCounts(TArray& Counts, int32 N); /** Section 6.4: buffer (max 24), one partial box per ore type, four outfeed slots. */ struct THEDESCENT_API FPackerState { TArray Buffer; int32 Partial[3] = { 0, 0, 0 }; int32 Packing = -1; // the ore type being packed, -1 = none double MoveTimer = 0.0, PackTimer = 0.0; int32 BoxesMade = 0; bool CanAccept(int32 InPipe) const { return Buffer.Num() + InPipe < Descent::Tune::Cargo::BufferMax; } /** bSlotFree: an outfeed slot is free. MakeBox(type, count) is called when a box is finished. */ void Step(double Dt, bool bSlotFree, TFunctionRef MakeBox); }; } /** An ore box (section 6.4): 0.6 x 0.4 x 0.4 m, one ore type, 1 to 6 pieces. Always in one place: a socket, the floor, or the player's hands. */ UCLASS() class THEDESCENT_API ADescentOreBox : public AActor, public IDescentInteractable { GENERATED_BODY() public: ADescentOreBox(); virtual void EndPlay(const EEndPlayReason::Type Reason) override; void Init(int32 InId, uint8 InType, int32 InCount); FString Label() const; int32 BoxId = 0; uint8 OreType = 0; int32 Count = 0; FString Where = TEXT("floor"); // "packer.out0".."out3", "floor", "hands" virtual FString GetPrompt() const override { return FString::Printf(TEXT("Carry %s"), *Label()); } virtual FVector GetFocusPointUU() const override { return GetActorLocation(); } virtual double GetRangeM() const override; virtual double GetMinDot() const override { return 0.5; } virtual void Use(ADescentCharacter* User) override; UPROPERTY(VisibleAnywhere) TObjectPtr Mesh; UPROPERTY(VisibleAnywhere) TObjectPtr Text; }; /** The refinery (section 6.5): a box goes into the hopper by hand, one piece at a time becomes a refined good in the carrier's storage. */ UCLASS() class THEDESCENT_API ADescentRefinery : public ADescentPlaceable { GENERATED_BODY() public: virtual void InitKit(const Descent::Build::FPlacement& P, FName InKit, int32 InId) override; virtual void EndPlay(const EEndPlayReason::Type Reason) override; virtual bool CanPickUp(FString* OutWhyNot = nullptr) const override; int32 Reserved = 0; // pieces an arm is bringing TArray Queue; // ore types waiting; Queue[0] is being refined double Progress = 0.0; // seconds into the current piece int32 RefineryId = 0; int32 Made[3] = { 0, 0, 0 }; bool CanTake(const ADescentOreBox* Box, FString* OutWhyNot = nullptr) const; void Step(double Dt); FVector HopperWorldUU() const; virtual FString GetPrompt() const override; virtual FVector GetFocusPointUU() const override { return HopperWorldUU(); } virtual double GetRangeM() const override { return 2.6; } virtual double GetMinDot() const override { return 0.4; } virtual void Use(ADescentCharacter* User) override; }; /** * The cargo loop of the carrier (M5, requirements U13, U14, V8): the unload job through the glass pipes, the packer and its boxes, * carrying, the refineries, the storage of refined goods, selling and the debt. Stepped in sim step 9 (Cargo). */ UCLASS() class THEDESCENT_API UDescentCargoSubsystem : public UDescentWorldSubsystem { GENERATED_BODY() public: static UDescentCargoSubsystem* Get(const UWorld* World); virtual void OnWorldBeginPlay(UWorld& InWorld) override; virtual void Deinitialize() override; // ---- unloading ---- bool IsUnloading() const { return UnloadTender.IsValid(); } int32 NumPipePieces() const { return Pieces.Num(); } /** How far the first piece in the pipe has come, metres (-1: none). */ double LeadPieceM() const; void AbortUnload(); // Space (launch) during the feed bool bFeedPaused = false; // ---- packer and boxes ---- Descent::Cargo::FPackerState Packer; ADescentOreBox* SpawnBox(uint8 Type, int32 Count, int32 Slot = -1); // Slot -1: the lowest free outfeed slot, -2: on the floor at the player bool EjectPartials(); int32 FreeSlot() const; ADescentOreBox* FindBox(int32 Id) const; const TArray>& Boxes() const { return BoxList; } void ForgetBox(ADescentOreBox* Box); FVector SlotLocalM(int32 Slot) const; int32 SlotBoxAt(int32 Slot) const { return Slot >= 0 && Slot < Descent::Tune::Cargo::OutfeedSlots ? SlotBoxId[Slot] : 0; } /** Frees whatever socket (outfeed slot, rack) holds this box. */ void ReleaseSockets(int32 BoxId); /** The box leaves the player's hands without being put anywhere yet. */ ADescentOreBox* ReleaseCarried(); void DropBoxAt(ADescentOreBox* Box, const FVector& LocalM); void DestroyAllBoxes(); /** Restores a box from a save: Where is "packer.outN", "rackID.N" or "floor" (then at LocalM). */ ADescentOreBox* RestoreBox(uint8 Type, int32 Count, const FString& Where, const FVector& LocalM); // ---- automation (M7): racks and arms register themselves ---- UPROPERTY(Transient) TArray> Racks; UPROPERTY(Transient) TArray> Arms; // ---- carrying (section 5.5) ---- ADescentOreBox* Carried() const { return CarriedBox.Get(); } bool PickUp(ADescentCharacter* User, ADescentOreBox* Box); bool PutDown(ADescentCharacter* User, FString* OutWhyNot = nullptr); bool LoadHopper(ADescentCharacter* User, ADescentRefinery* Refinery, FString* OutWhyNot = nullptr); // ---- refineries ---- ADescentRefinery* SpawnRefinery(const FVector& LocalM, double YawDeg); void Register(ADescentRefinery* R) { Refineries.AddUnique(R); if (R && R->RefineryId == 0) { R->RefineryId = NextRefineryId++; } } void Unregister(ADescentRefinery* R) { Refineries.Remove(R); } const TArray>& GetRefineries() const { return Refineries; } // ---- storage, ledger, money ---- int32 Refined[3] = { 0, 0, 0 }; int32 StorageUsed() const { return Refined[0] + Refined[1] + Refined[2]; } int32 StorageCap = Descent::Tune::Cargo::StorageCap; int32 CountOre(uint8 Type) const; int32 ConsumeOre(uint8 Type, int32 N); double Earned = 0.0; int32 Sold = 0; double Debt() const; bool SellRefined(uint8 Type, int32 N); bool SellBox(int32 BoxId); private: void Step(double Dt); void StepUnload(double Dt); void EnsureVisuals(); void UpdateVisuals(); void FillWindows(); ADescentCarrier* Carrier() const; struct FPiece { double Dist = 0.0; uint8 Type = 0; }; TArray Pieces; TWeakObjectPtr UnloadTender; double PhaseTimer = 0.0, FeedTimer = 0.0, FullToastTimer = 0.0, WindowTimer = 0.0; int32 UnloadPhase = 0; // 0 couple, 1 feed, 2 decouple int32 UnloadedCount = 0; int32 NextBoxId = 1, NextRefineryId = 1; int32 SlotBoxId[Descent::Tune::Cargo::OutfeedSlots] = { 0, 0, 0, 0 }; TWeakObjectPtr CarriedBox; FDescentHandle StepHandle; UPROPERTY(Transient) TArray> BoxList; UPROPERTY(Transient) TArray> Refineries; UPROPERTY(Transient) TObjectPtr PipeActor; UPROPERTY(Transient) TArray> PieceMeshes; };