#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Core/DescentHandle.h" #include "Core/DescentWorldSubsystem.h" #include "Player/DescentInteraction.h" #include "DescentBuild.generated.h" class ADescentCarrier; class ADescentCharacter; class UPointLightComponent; class UStaticMeshComponent; /** Build and placement mode (M7, requirement U1, DESIGN_UE.md section 6.10): the kit table and the validity rules as pure functions. Carrier-local metres. */ namespace Descent::Build { enum EMount : uint8 { Floor = 1, Wall = 2, Ceiling = 4 }; struct FKitDef { FName Id; FString Name; FVector SizeM = FVector::OneVector; // x (front is +x) by y by z uint8 Mount = Floor; }; THEDESCENT_API const TArray& Kits(); /** "table" and "kit_table" both name the table. */ THEDESCENT_API const FKitDef* FindKit(FName Id); THEDESCENT_API FString MountText(uint8 Mount); constexpr int32 MaxPlaced = 200; /** Where a kit would stand: the actor pose (carrier-local), the body box inside the actor, and its axis-aligned bounds. */ struct FPlacement { FVector PosM = FVector::ZeroVector; double YawDeg = 0.0; uint8 Surface = Floor; FVector BodyOffsetM = FVector::ZeroVector, BodySizeM = FVector::OneVector; // in the actor's axes FVector CentreM = FVector::ZeroVector, HalfM = FVector::OneVector; // bounds in carrier axes FBox Bounds(double Shrink = 0.02) const { return FBox(CentreM - HalfM + FVector(Shrink), CentreM + HalfM - FVector(Shrink)); } }; /** Surface: which mount the hit surface is (0 = none of them). */ THEDESCENT_API uint8 SurfaceOf(const FVector& Normal); THEDESCENT_API FPlacement MakePlacement(const FKitDef& Kit, const FVector& HitM, const FVector& Normal, double YawDeg, bool bSnap); THEDESCENT_API FPlacement MakePlacementAt(const FKitDef& Kit, const FVector& PosM, double YawDeg, uint8 Surface); // The six rules of section 6.10; each returns true when the placement is allowed. THEDESCENT_API bool RuleMount(const FKitDef& Kit, const FVector& Normal); THEDESCENT_API bool RuleInRoom(const FPlacement& P, FName* OutRoom = nullptr); THEDESCENT_API bool RuleKeepOut(const FPlacement& P); THEDESCENT_API bool RuleNoOverlap(const FPlacement& P, TConstArrayView Blockers); THEDESCENT_API bool RuleFloorCorners(const FPlacement& P); THEDESCENT_API bool RuleCount(int32 NumPlaced); /** All rules in order; Reason is the first that fails (BAD_SURFACE, OUT_OF_ROOM, KEEP_OUT, BLOCKED, OVERHANG, LIMIT_200). */ THEDESCENT_API bool Validate(const FKitDef& Kit, const FVector& HitM, const FVector& Normal, double YawDeg, bool bSnap, TConstArrayView Blockers, int32 NumPlaced, FPlacement& Out, FString& Reason); } /** Anything placed by the player (and the start fabricator and refinery). The actor origin is on the surface it is mounted on; the front is +X. */ UCLASS() class THEDESCENT_API ADescentPlaceable : public AActor, public IDescentInteractable { GENERATED_BODY() public: ADescentPlaceable(); virtual void EndPlay(const EEndPlayReason::Type Reason) override; /** Builds the blockout and registers with the systems the kit serves. */ virtual void InitKit(const Descent::Build::FPlacement& P, FName InKit, int32 InId); virtual bool CanPickUp(FString* OutWhyNot = nullptr) const; virtual void OnPickedUp(); FName KitId; int32 PlaceId = 0; Descent::Build::FPlacement Place; FName RoomId() const; FVector LocalM(const FVector& ActorLocalM) const; // a point in the actor's axes, as carrier-local metres FVector WorldUUOf(const FVector& ActorLocalM) const; virtual FString GetPrompt() const override; virtual FVector GetFocusPointUU() const override; virtual double GetRangeM() const override { return 2.8; } virtual double GetMinDot() const override { return 0.5; } virtual void Use(ADescentCharacter* User) override; UPROPERTY(VisibleAnywhere) TObjectPtr Root; UPROPERTY(VisibleAnywhere) TObjectPtr Body; UPROPERTY(VisibleAnywhere) TObjectPtr Face; UPROPERTY(VisibleAnywhere) TObjectPtr Lamp; protected: UStaticMeshComponent* AddPart(const TCHAR* Name, const FVector& OffsetM, const FVector& SizeM, const FLinearColor& Base, const FLinearColor& Emissive = FLinearColor::Black, float Strength = 0.f, bool bSolid = false, USceneComponent* Parent = nullptr); }; /** The kits in storage, the ghost, placing and picking up. */ UCLASS() class THEDESCENT_API UDescentBuildSubsystem : public UDescentWorldSubsystem { GENERATED_BODY() public: static UDescentBuildSubsystem* Get(const UWorld* World); virtual void OnWorldBeginPlay(UWorld& InWorld) override; virtual void Deinitialize() override; // ---- kits ---- int32 KitCount(FName Kit) const; void AddKit(FName Kit, int32 N); const TMap& AllKits() const { return KitCounts; } void ResetKits() { KitCounts.Reset(); } // ---- the ghost ---- bool BeginGhost(FName Kit, FString* OutWhyNot = nullptr); void CancelGhost(); bool IsGhosting() const { return !GhostKit.IsNone(); } void RotateGhost(int32 Steps) { GhostYawDeg = FRotator::NormalizeAxis(GhostYawDeg + 15.0 * Steps); } /** Debug: pin the ghost at a carrier-local point on a surface with this normal, instead of the eye ray. */ void SetDebugAim(const FVector& LocalM, const FVector& Normal) { bDebugAim = true; DebugAimM = LocalM; DebugNormal = Normal; } bool ConfirmGhost(FString* OutWhyNot = nullptr); FName GhostKit; bool bGhostValid = false, bGhostHit = false, bFreePlacement = false; FString GhostReason; Descent::Build::FPlacement GhostPlace; // ---- placed objects ---- ADescentPlaceable* Spawn(FName Kit, const FVector& PosM, double YawDeg, uint8 Surface = Descent::Build::Floor); bool PickUp(ADescentPlaceable* P, FString* OutWhyNot = nullptr); void DestroyAll(); void Forget(ADescentPlaceable* P) { PlacedList.Remove(P); } const TArray>& Placed() const { return PlacedList; } ADescentPlaceable* FindPlaced(int32 Id) const; ADescentPlaceable* FindFirst(FName Kit) const; int32 PlacedByPlayer = 0, PickedUp = 0; void OpenMenu(); private: void Step(double Dt); void UpdateGhost(); ADescentCarrier* Carrier() const; TMap KitCounts; double GhostYawDeg = 0.0; bool bDebugAim = false; FVector DebugAimM = FVector::ZeroVector, DebugNormal = FVector::UpVector; int32 NextId = 1; FDescentHandle StepHandle; UPROPERTY(Transient) TArray> PlacedList; UPROPERTY(Transient) TObjectPtr GhostActor; UPROPERTY(Transient) TObjectPtr GhostMesh; UPROPERTY(Transient) TObjectPtr GhostMID; };