#pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "Core/DescentIntent.h" #include "UI/ViewModels/CargoViewModel.h" #include "UI/ViewModels/DashViewModel.h" #include "TenderFlight.generated.h" /** * The Tender's numbers and flight (DESIGN_UE.md sections 4.2, 4.3; v2 subStats / updateSub). The pure part (specs, stats, crush, * collision spheres) is plain functions so the automation tests need no world; UTenderFlightComponent is the state and the step. */ namespace Descent::Tender { /** One row of the fleet table of section 4.1. */ struct FTenderSpec { const TCHAR* Id; // "BT-01" const TCHAR* Name; // "TENDER" double Speed; // multiplier double DrillR; // metres added to the drill radius double DrillRate; // multiplier int32 Hold; FLinearColor Paint; // overrides the "Pearl ceramic lacquer" colour }; THEDESCENT_API const FTenderSpec& Spec(int32 Index); /** The upgrade levels a Tender's numbers depend on (milestone M4 sells them; M3 only reads zeros). */ struct FTenderUpgrades { int32 HullMk = 0, DepthMk = 0, ThrustMk = 0, HoldMk = 0, DrillMk = 0, SuctionMk = 0; bool bPowerCell2 = false, bSonarModule = false, bLaser = false; }; /** Section 4.2: the only source of every Tender number. */ struct FTenderStats { double HullMax = 100.0, Rating = 2300.0, Capacity = 100.0, Speed = 9.0, BioCap = 4.0, DrillR = 3.0, DrillRate = 9.0; double SonarRange = 160.0, SuctionRange = 13.0, SuctionPull = 9.0; int32 HoldCap = 8; }; THEDESCENT_API FTenderStats ComputeStats(const FTenderSpec& S, const FTenderUpgrades& U); /** Section 4.3: (0.6 + over / 25) HP per second below the rating, 0 above it. */ THEDESCENT_API double CrushDamagePerSec(double DepthM, double RatingM); struct FCollisionSphere { FVector CentreM; double RadiusM; }; /** Section 4.3: nose, cockpit, centre, stern, two propulsors, two tanks, the tool. */ THEDESCENT_API TConstArrayView CollisionSpheres(); /** True inside one of the two honest voids (full-pressure chambers, section 2.9). */ THEDESCENT_API bool InHonestVoid(const FVector& WorldM); } /** What the Tender is doing; the carousel owns the first three, the flight step the last two. */ enum class ETenderState : uint8 { Cradle, Launching, Docking, Piloted, Abandoned }; THEDESCENT_API const TCHAR* TenderStateName(ETenderState S); UCLASS(ClassGroup = (Descent)) class THEDESCENT_API UTenderFlightComponent : public UActorComponent { GENERATED_BODY() public: UTenderFlightComponent(); // ---- identity and numbers ---- int32 SpecIndex = 0; Descent::Tender::FTenderUpgrades Upgrades; const Descent::Tender::FTenderSpec& Spec() const { return Descent::Tender::Spec(SpecIndex); } Descent::Tender::FTenderStats Stats() const { return Descent::Tender::ComputeStats(Spec(), Upgrades); } // ---- state ---- ETenderState State = ETenderState::Cradle; double HullHP = 100.0; // absolute HP (section 4.2) double PowerPct = 100.0; // percent of the capacity FVector VelMps = FVector::ZeroVector; double YawDeg = 0.0, PitchDeg = 0.0, RollRad = 0.0; double Thrust = 0.0; // 0..1.7: |wish| x boost; drives the rotors int32 Contacts = 0; // collision spheres that touched rock (or the carrier) in the last step int64 ContactsTotal = 0; double LastHitMps = 0.0; bool bLamp = true; bool bImploded = false; // hull reached 0: the controller turns this into the death "implode" bool bInHonestVoid = false; TArray HoldOrder; // chunks in arrival order (M4 fills it) double BioFu = 0.0; double BioFullToast = 0.0; bool IsPowered() const { return PowerPct > 0.0; } bool IsOut() const { return State == ETenderState::Piloted || State == ETenderState::Abandoned; } double DepthM() const; FVector PosM() const; /** Sets yaw and pitch from the actor (after a teleport or when the carousel lets go). */ void SyncFromActor(); void ResetAsNew(double InPowerPct); /** bImpactToast: tells the pilot "Hull impact -n%". */ void ApplyDamage(double HP, bool bImpactToast = false); /** One sim step of a Tender that is out: Intent null = abandoned (zero thrust). */ void StepFlight(const FDescentIntent* Intent, double Dt, double HungerMul = 1.0); // ---- the tool (section 4.5; TenderTool.cpp) ---- bool bDrilling = false, bSucking = false, bBasalt = false, bLaserHead = false; int32 DebugDrill = -1, DebugSuck = -1; // Descent.Tender.Drill / .Suck: -1 = the intent decides int64 DrillRemovedTotal = 0; int32 CapturedTotal = 0; FVector DrillHitM = FVector::ZeroVector; bool bDrillHit = false; /** One sim step of the tool of a Tender that is out: LMB drills, RMB sucks, the drill wins. Intent null = nobody aboard. */ void StepTool(const FDescentIntent* Intent, double Dt); /** Puts one chunk into the hold; false (and the HOLD FULL toast) when it is full. */ bool AddToHold(FName Ore); /** One sim step in a cradle: recharge and repair. */ void StepDocked(double Dt); /** Section 4.6 "one source of truth": the dash, the cargo display, the HUD vehicle panel and the state section read this. */ FTenderDashModel MakeDashModel() const; FCargoDisplayModel MakeCargoModel() const; private: void Collide(FVector& PosM, const FRotator& Rot); double CrushAlarmTimer = 0.0; double CleanupTimer = 0.0, HoldFullTimer = 0.0; bool bPowerWarned = false; };