#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Core/DescentHandle.h" #include "Core/DescentWorldSubsystem.h" #include "Player/DescentInteraction.h" #include "KazahTender.generated.h" class UBoxComponent; class UCameraComponent; class UDescentDashComponent; class UMaterialInstanceDynamic; class UPointLightComponent; class UStaticMeshComponent; class USpotLightComponent; class UTenderFlightComponent; class AKazahTender; /** One exported part of the Tender. The table of these is generated by Tools/blender/export_kazah.py. */ struct FKazahPartDef { /** Component name and Blender object name: "Rotor_Port", "Canopy_Glass", ... */ const TCHAR* Name; /** Asset under /Game/Vehicles/Kazah/, e.g. "SM_Kazah_Rotor_Port". */ const TCHAR* MeshAsset; /** Where the part's pivot sits in the vehicle, cm, Unreal axes (+X forward, +Y right, +Z up). */ FVector PivotCm; /** Bounds of the part's mesh around its pivot, cm. Used by tests to catch a bad re-export. */ FVector LocalBoundsMinCm; FVector LocalBoundsMaxCm; }; /** * The fleet (DESIGN_UE.md section 1.4, sim step 4): steps every Tender that belongs to the game (flight of the piloted one, drift of * abandoned ones, recharge in a cradle) and feeds the cockpit dashboards in the Dashboards late step. Tenders that are only looked at * (the showcase rig) are not in it. */ UCLASS() class THEDESCENT_API UDescentTenderFleet : public UDescentWorldSubsystem { GENERATED_BODY() public: static UDescentTenderFleet* Get(const UWorld* World); virtual void OnWorldBeginPlay(UWorld& InWorld) override; virtual void Deinitialize() override; void Add(AKazahTender* T); const TArray>& All() const { return Tenders; } AKazahTender* At(int32 Index) const { return Tenders.IsValidIndex(Index) ? Tenders[Index].Get() : nullptr; } /** The Tender the player flies right now (state Piloted), or null. Set by the player controller. */ TWeakObjectPtr Piloted; /** The speed penalty of hunger (section 5.8); 1 until nutrition exists (M8). */ double HungerMul = 1.0; private: TArray> Tenders; FDescentHandle StepHandle, DashHandle, ToolHandle; }; /** * The small submarine ("Subtub", the Kazah Tender), DESIGN_UE.md section 4. It assembles the imported parts as components and, when it * belongs to the fleet (InitFleet), flies: flight component, collision boxes against the player, headlamp, cockpit camera and * dashboards, lids, beacon, paint. * * Frame: +X forward (the nose), +Y starboard (right), +Z up. The origin is the Blender file's world origin: * 3.73 m behind the tip of the nose, 3.26 m ahead of the tail, 0.97 m above the keel and 1.55 m below the top of * the hatch collar. 6.99 m long, 3.27 m wide, 2.51 m tall. * * Each moving part is its own component whose origin is on its axis of motion: * Rotor_Port / Rotor_Stbd spin about the X axis (roll) through the hub; they counter-rotate * Canopy_Lid, Canopy_Glass share one hinge line (lateral, Y axis) at the rear-bottom edge of the lid * Hatch_Lid hinge line at the aft edge of the roof hatch * Tool_Housing, Nose_Lens, Chin_Sensor pivot at their centres * The meshes come from /Game/Vehicles/Kazah/ (created by Tools/import_assets.ps1); a missing mesh is logged * once and the part is simply absent. */ UCLASS() class THEDESCENT_API AKazahTender : public AActor, public IDescentInteractable { GENERATED_BODY() public: AKazahTender(); virtual void OnConstruction(const FTransform& Transform) override; virtual void Tick(float DeltaSeconds) override; virtual void EndPlay(const EEndPlayReason::Type Reason) override; /** Revolutions per minute of the rotors (the port one turns the opposite way). 0 stops them. A fleet Tender sets it from its thrust. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tender|Propulsion", meta = (ClampMin = "-3000", ClampMax = "3000")) float RotorRPM = 420.f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tender|Headlight") bool bHeadlightOn = true; /** Multiplies the start intensity of the Tender headlamp of DESIGN 7.5 (3.2, set up by Descent::Look::ConfigureSpot); the horror flicker uses it. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tender|Headlight", meta = (ClampMin = "0")) float HeadlightIntensityScale = 1.f; UFUNCTION(BlueprintCallable, Category = "Tender|Headlight") void SetHeadlight(bool bOn); UFUNCTION(BlueprintPure, Category = "Tender|Propulsion") float GetRotorAngleDeg() const { return RotorAngleDeg; } /** Degrees the rotors have turned in total since spawn (not wrapped); a scenario can assert it grew. */ UFUNCTION(BlueprintPure, Category = "Tender|Propulsion") double GetRotorTotalDeg() const { return RotorTotalDeg; } /** Advances the rotor pose by DeltaSeconds as Tick would. Public so tests can drive it without a world. */ void AdvanceRotors(float DeltaSeconds); UStaticMeshComponent* GetPart(FName PartName) const; /** The generated part table. */ static TConstArrayView GetPartDefs(); static FVector GetModelBoundsMinCm(); static FVector GetModelBoundsMaxCm(); // ---- the flying Tender (M3) ---------------------------------------------------------------------------------- UPROPERTY(VisibleAnywhere) TObjectPtr Flight; UPROPERTY(VisibleAnywhere) TObjectPtr CockpitCamera; /** Makes this Tender number Index of the fleet: spec, paint, dashboards, interaction, rotors driven by thrust. */ void InitFleet(int32 Index); int32 GetFleetIndex() const { return FleetIndex; } /** In the bay: lit by the bay lamps (channel 1) and "dry" for the water post-process (stencil 1). At sea: channel 0, no stencil. */ void SetInBay(bool bIn); bool IsInBay() const { return bInBay; } void SetCanopyOpen(bool bOpen) { bCanopyTarget = bOpen; } void SetHatchOpen(bool bOpen) { bHatchTarget = bOpen; } double GetCanopyOpen01() const { return Canopy01; } double GetHatchOpen01() const { return Hatch01; } /** Docked: the pilot looks around the cockpit (degrees, clamped to section 4.6). Zero looks along the nose. */ void SetCockpitLook(double YawDeg, double PitchDeg); FVector2D GetCockpitLook() const { return CockpitLook; } /** Writes the dashboards from the flight component's model (Dashboards late step). */ void UpdateDashboards(); UDescentDashComponent* GetDashMain() const { return DashMain; } UDescentDashComponent* GetDashCargo() const { return DashCargo; } FVector ToWorldUU(const FVector& LocalM) const { return GetActorTransform().TransformPosition(LocalM * 100.0); } // ---- IDescentInteractable: E on foot boards the Tender that hangs at BOARD ---- virtual FString GetPrompt() const override; virtual FVector GetFocusPointUU() const override; virtual double GetRangeM() const override; virtual double GetMinDot() const override { return 0.3; } virtual void Use(ADescentCharacter* User) override; private: void ApplyRotorPose(); void ApplyHeadlight(); void ApplyLids(); void ApplyRenderChannels(); UPROPERTY() TObjectPtr Root; UPROPERTY() TMap> Parts; UPROPERTY() TObjectPtr Headlight; UPROPERTY() TObjectPtr CockpitGlow; /** The glow at the point the drill bites (section 4.5): colour (1.0, 0.8, 0.55), radius 9 m; white on dense basalt. */ UPROPERTY() TObjectPtr DrillGlow; UPROPERTY() TObjectPtr Beacon; UPROPERTY() TArray> CollisionBoxes; UPROPERTY(Transient) TObjectPtr DashMain; UPROPERTY(Transient) TObjectPtr DashCargo; UPROPERTY(Transient) TObjectPtr LensMID; UPROPERTY(Transient) TObjectPtr BeaconMID; float RotorAngleDeg = 0.f; double RotorTotalDeg = 0.0; bool bLoggedMissingMesh = false; int32 FleetIndex = -1; bool bInBay = false; bool bCanopyTarget = false, bHatchTarget = false; double Canopy01 = 0.0, Hatch01 = 0.0; FVector2D CockpitLook = FVector2D::ZeroVector; double BeaconClock = 0.0; bool bLensOn = true; };