#include "Vehicles/TenderFlight.h" #include "Vehicles/CarrierHullShape.h" #include "Vehicles/DescentCarrier.h" #include "Core/DescentEnvQuery.h" #include "Core/DescentMath.h" #include "Core/Tuning/DescentTune_Tender.h" #include "DescentUnits.h" #include "TheDescent.h" #include "UI/DescentHudSubsystem.h" #include "World/DescentShear.h" #include "World/DescentTerrainSubsystem.h" #include "World/DescentWorldDef.h" #include "Engine/World.h" #include "GameFramework/Actor.h" namespace TTune = Descent::Tune::Tender; namespace Descent::Tender { const FTenderSpec& Spec(int32 Index) { // Section 4.1: all three are the Kazah model; BT-01 keeps the modelled lacquer. static const FTenderSpec Specs[TTune::FleetCount] = { { TEXT("BT-01"), TEXT("TENDER"), 1.0, 0.0, 1.0, 8, FLinearColor(0.638f, 0.711f, 0.674f) }, { TEXT("BT-02"), TEXT("DART"), 1.3, -0.4, 0.85, 6, FLinearColor(0.03f, 0.35f, 0.33f) }, { TEXT("BT-03"), TEXT("MOLE"), 0.8, 0.9, 1.4, 12, FLinearColor(0.60f, 0.12f, 0.03f) }, }; return Specs[FMath::Clamp(Index, 0, TTune::FleetCount - 1)]; } FTenderStats ComputeStats(const FTenderSpec& S, const FTenderUpgrades& U) { FTenderStats Out; Out.HullMax = TTune::HullBaseHP * (1.0 + TTune::HullPerMk * U.HullMk); Out.Rating = U.DepthMk >= 2 ? TTune::RatingMk2M : (U.DepthMk == 1 ? TTune::RatingMk1M : TTune::RatingBaseM); Out.Capacity = U.bPowerCell2 ? TTune::CapacityCell2 : TTune::CapacityBase; Out.Speed = TTune::SpeedBaseMps * S.Speed * (1.0 + TTune::SpeedPerThrustMk * U.ThrustMk); Out.HoldCap = S.Hold + TTune::HoldPerMk * U.HoldMk; Out.BioCap = TTune::BioCapFu; Out.DrillR = TTune::DrillRBase + TTune::DrillRPerMk * U.DrillMk + S.DrillR; Out.DrillRate = TTune::DrillRateBase * (1.0 + TTune::DrillRatePerMk * U.DrillMk) * S.DrillRate; Out.SonarRange = U.bSonarModule ? TTune::SonarRangeModuleM : TTune::SonarRangeM; Out.SuctionRange = U.SuctionMk >= 1 ? TTune::SuctionRangeMk2M : TTune::SuctionRangeM; Out.SuctionPull = U.SuctionMk >= 1 ? TTune::SuctionPullMk2 : TTune::SuctionPull; return Out; } double CrushDamagePerSec(double DepthM, double RatingM) { const double Over = DepthM - RatingM; return Over > 0.0 ? TTune::CrushBaseHPPerSec + Over / TTune::CrushOverDivisor : 0.0; } TConstArrayView CollisionSpheres() { static const FCollisionSphere Spheres[] = { { FVector(2.6, 0.0, 0.3), 1.0 }, // nose { FVector(1.2, 0.0, 0.3), 1.15 }, // cockpit { FVector(-0.3, 0.0, 0.2), 1.35 }, // centre { FVector(-2.2, 0.0, 0.15), 1.1 }, // stern { FVector(-2.55, -1.09, 0.13), 0.55 }, // propulsors { FVector(-2.55, 1.09, 0.13), 0.55 }, { FVector(0.0, -1.0, -0.45), 0.5 }, // tanks { FVector(0.0, 1.0, -0.45), 0.5 }, { FVector(1.97, TTune::ToolSide * 0.69, -0.78), 0.3 }, // the tool }; return TConstArrayView(Spheres); } bool InHonestVoid(const FVector& P) { for (const Descent::World::FChamber& Ch : Descent::World::FDescentWorldDef::Get().Chambers) { if (Ch.bHonest && ((P - Ch.C) / Ch.R).SizeSquared() < 1.0) { return true; } } return false; } } const TCHAR* TenderStateName(ETenderState S) { static const TCHAR* const Names[] = { TEXT("Cradle"), TEXT("Launching"), TEXT("Docking"), TEXT("Piloted"), TEXT("Abandoned") }; return Names[static_cast(S)]; } UTenderFlightComponent::UTenderFlightComponent() { PrimaryComponentTick.bCanEverTick = false; // section 1.4: state changes only in the sim step } FVector UTenderFlightComponent::PosM() const { return GetOwner() ? Descent::UUToMetres(GetOwner()->GetActorLocation()) : FVector::ZeroVector; } double UTenderFlightComponent::DepthM() const { return Descent::DepthOfZ(PosM().Z); } void UTenderFlightComponent::SyncFromActor() { if (const AActor* A = GetOwner()) { const FRotator R = A->GetActorRotation(); YawDeg = R.Yaw; PitchDeg = FMath::Clamp(FRotator::NormalizeAxis(R.Pitch), -TTune::PitchLimitDeg, TTune::PitchLimitDeg); RollRad = 0.0; } } void UTenderFlightComponent::ResetAsNew(double InPowerPct) { HullHP = Stats().HullMax; PowerPct = InPowerPct; VelMps = FVector::ZeroVector; Thrust = 0.0; Contacts = 0; LastHitMps = 0.0; bImploded = false; bInHonestVoid = false; bPowerWarned = false; HoldOrder.Reset(); BioFu = 0.0; bDrilling = bSucking = bBasalt = false; } void UTenderFlightComponent::ApplyDamage(double HP, bool bImpactToast) { if (HP <= 0.0 || bImploded) { return; } const double Max = Stats().HullMax; HullHP = FMath::Max(0.0, HullHP - HP); if (bImpactToast && State == ETenderState::Piloted) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().PushToast(FString::Printf(TEXT("Hull impact -%d%%"), FMath::RoundToInt32(100.0 * HP / Max)), EDescentHudToastKind::Bad); } } if (HullHP <= 0.0) { bImploded = true; UE_LOG(LogDescent, Display, TEXT("Tender %s: hull 0 at depth %.0f m (implosion)"), Spec().Id, DepthM()); } } void UTenderFlightComponent::Collide(FVector& P, const FRotator& Rot) { UWorld* World = GetWorld(); UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(World); Contacts = 0; double Hit = 0.0; if (Terrain) { // Section 4.3: spheres against the density field, two iterations. Gradient points into rock. TBitArray<> Touched(false, Descent::Tender::CollisionSpheres().Num()); for (int32 Iter = 0; Iter < TTune::TerrainIterations; ++Iter) { int32 Index = 0; for (const Descent::Tender::FCollisionSphere& S : Descent::Tender::CollisionSpheres()) { const FVector C = P + Rot.RotateVector(S.CentreM); const double Pen = S.RadiusM + Terrain->Density(C); if (Pen > 0.0) { const FVector G = Terrain->Gradient(C); P -= G * (TTune::TerrainPushFactor * Pen); const double Vn = FVector::DotProduct(VelMps, G); if (Vn > 0.0) { VelMps -= G * (Vn * TTune::TerrainBounce); Hit = FMath::Max(Hit, Vn); } Touched[Index] = true; } ++Index; } } Contacts = Touched.CountSetBits(); } // The carrier: an analytic hull (it has no physics collision), radius 2 m around the origin, velocity relative to the carrier. if (const ADescentCarrier* Carrier = UDescentCarrierSubsystem::GetCarrier(World)) { FVector Local = Carrier->ToLocalM(Descent::MetresToUU(P)); FVector NLocal; if (Descent::Carrier::HullPushOut(Local, TTune::CarrierRadiusM, /*bOpenPool*/ false, &NLocal)) { P = Descent::UUToMetres(Carrier->ToWorldUU(Local)); const FVector N = Carrier->GetActorTransform().TransformVectorNoScale(NLocal); const FVector Rel = VelMps - Carrier->VelocityMps(); const double Vn = -FVector::DotProduct(Rel, N); // N points away from the hull if (Vn > 0.0) { VelMps += N * (Vn * TTune::TerrainBounce); Hit = FMath::Max(Hit, Vn); } ++Contacts; } } if (Contacts > 0) { ContactsTotal += Contacts; } LastHitMps = Hit; if (Hit > TTune::ImpactSpeedMps) { ApplyDamage((Hit - TTune::ImpactSpeedMps) * TTune::ImpactHPPerMps, true); } } void UTenderFlightComponent::StepFlight(const FDescentIntent* Intent, double Dt, double HungerMul) { AActor* Owner = GetOwner(); if (!Owner || Dt <= 0.0) { return; } const Descent::Tender::FTenderStats St = Stats(); FVector Wish = FVector::ZeroVector; bool bBoost = false; double Strafe = 0.0, LookX = 0.0; if (Intent && !bImploded) { // Section 4.3: the mouse sets yaw and pitch directly (the controller already scaled the look rate to 0.102 deg per pixel). YawDeg = FRotator::NormalizeAxis(YawDeg + Intent->Look.X * Dt); PitchDeg = FMath::Clamp(PitchDeg + Intent->Look.Y * Dt, -TTune::PitchLimitDeg, TTune::PitchLimitDeg); const FRotator Aim(PitchDeg, YawDeg, 0.0); Wish = Aim.RotateVector(FVector(Intent->Move.X, 0.0, 0.0)) + FRotator(0.0, YawDeg, 0.0).RotateVector(FVector(0.0, Intent->Move.Y, 0.0)) + FVector(0.0, 0.0, Intent->Move.Z); if (Wish.SizeSquared() > 1.0) { Wish.Normalize(); } bBoost = Intent->bSprint && IsPowered() && !Wish.IsNearlyZero(); Strafe = Intent->Move.Y; LookX = Intent->Look.X; } Thrust = Wish.Size() * (bBoost ? TTune::BoostFactor : 1.0); const double MaxS = St.Speed * (bBoost ? TTune::BoostFactor : 1.0) * HungerMul * (IsPowered() ? 1.0 : TTune::UnpoweredFactor); const double A = 1.0 - FMath::Exp(-TTune::VelocityK * Dt); VelMps = FMath::Lerp(VelMps, Wish * MaxS, A); // cosmetic bank: into a strafe and into a turn (v2 measured the mouse in pixels per 60 Hz frame) const double LookPx = LookX / (Descent::Tune::Tender::SteerScale * 0.12) / 60.0; const double RollTarget = TTune::RollPerStrafeRad * Strafe + FMath::Clamp(TTune::RollPerLookPx * LookPx, -TTune::RollLookLimitRad, TTune::RollLookLimitRad); RollRad = Descent::Damp(RollRad, RollTarget, TTune::RollDampK, Dt); // Position: velocity plus the Shear. NO CLAMP OF ANY KIND (requirement U7). FVector P = PosM(); P += (VelMps + Descent::World::FDescentShear::VelocityAt(P)) * Dt; const FRotator Rot(PitchDeg, YawDeg, 0.0); Collide(P, Rot); Owner->SetActorLocationAndRotation(Descent::MetresToUU(P), FRotator(PitchDeg, YawDeg, FMath::RadiansToDegrees(RollRad)), false, nullptr, ETeleportType::None); // Energy (units per second; power is a percentage of the capacity). double Drain = TTune::DrainIdle + (bBoost ? TTune::DrainBoost : 0.0) + (bLamp ? TTune::DrainHeadlight : 0.0); if (!Intent) { Drain = bLamp ? TTune::DrainHeadlight : 0.0; // an abandoned Tender only feeds its lamp } PowerPct = FMath::Max(0.0, PowerPct - 100.0 * Drain / St.Capacity * Dt); if (PowerPct <= TTune::PowerWarnPct && !bPowerWarned && Intent) { bPowerWarned = true; if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().SetRadio(TEXT("Tender power at twenty percent. Return to the cradle."), 6.f); } } if (PowerPct > TTune::PowerWarnPct + 5.0) { bPowerWarned = false; } if (!IsPowered()) { bLamp = false; } // Crush depth and the honest voids. const double Crush = Descent::Tender::CrushDamagePerSec(Descent::DepthOfZ(P.Z), St.Rating); if (Crush > 0.0) { ApplyDamage(Crush * Dt); CrushAlarmTimer -= Dt; if (CrushAlarmTimer <= 0.0) { CrushAlarmTimer = TTune::CrushAlarmEverySec; // the alarm and creak sounds hang here (audio is M9) } } bInHonestVoid = Descent::Tender::InHonestVoid(P); if (bInHonestVoid) { ApplyDamage(TTune::HonestVoidHPPerSec * Dt); } } void UTenderFlightComponent::StepDocked(double Dt) { const Descent::Tender::FTenderStats St = Stats(); PowerPct = FMath::Min(100.0, PowerPct + TTune::DockedPowerPctPerSec * Dt); HullHP = FMath::Min(St.HullMax, HullHP + TTune::DockedHullHPPerSec * Dt); VelMps = FVector::ZeroVector; Thrust = 0.0; Contacts = 0; bInHonestVoid = false; } FTenderDashModel UTenderFlightComponent::MakeDashModel() const { const Descent::Tender::FTenderStats St = Stats(); FTenderDashModel M; M.Title = FString::Printf(TEXT("%s %s"), Spec().Id, Spec().Name); M.ToolText = bBasalt ? TEXT("DENSE BASALT — NO PURCHASE") : (bSucking ? TEXT("SUCTION") : (bLaserHead ? TEXT("LASER") : TEXT("DRILL"))); M.DepthM = DepthM(); // the Tender origin in every sub mode (section 5.9) M.RatingM = St.Rating; M.HullPct = static_cast(FMath::RoundToDouble(100.0 * HullHP / St.HullMax)); M.PowerPct = static_cast(PowerPct); M.TempC = GetOwner() ? Descent::AmbientTemperatureAt(GetWorld(), GetOwner()->GetActorLocation()) : 0.0; M.SpeedMps = static_cast(VelMps.Size()); M.HoldUsed = static_cast(HoldOrder.Num()); M.HoldCap = static_cast(St.HoldCap); M.BioFu = static_cast(BioFu); M.BioCapFu = static_cast(St.BioCap); return M; } FCargoDisplayModel UTenderFlightComponent::MakeCargoModel() const { const Descent::Tender::FTenderStats St = Stats(); FCargoDisplayModel M; M.Cap = St.HoldCap; M.Order = HoldOrder; M.BioFu = static_cast(BioFu); M.BioCapFu = static_cast(St.BioCap); return M; }