#include "Player/DescentCharacter.h" #include "Player/DescentCharacterMovement.h" #include "Player/DescentInteraction.h" #include "Player/DescentMode.h" #include "Player/DescentPlayerController.h" #include "Player/DescentVitals.h" #include "Systems/DescentCargo.h" #include "Player/Arm/DescentArmViewmodelComponent.h" #include "Player/Body/DescentPlayerBodyComponent.h" #include "Vehicles/KazahTender.h" #include "Core/Tuning/DescentTune_Tender.h" #include "Core/DescentGameClasses.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Core/Tuning/DescentTune_Player.h" #include "Debug/DescentCommandUtil.h" #include "Vehicles/CarrierLayout.h" #include "Vehicles/DescentCarrier.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Camera/CameraComponent.h" #include "Components/CapsuleComponent.h" #include "Components/SkeletalMeshComponent.h" #include "Components/PointLightComponent.h" #include "Components/SpotLightComponent.h" #include "Vehicles/CarrierBlockout.h" #include "GameFramework/CharacterMovementComponent.h" #include "Engine/World.h" namespace { namespace T = Descent::Tune::Player; } ADescentCharacter::ADescentCharacter(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer.SetDefaultSubobjectClass(ACharacter::CharacterMovementComponentName)) { PrimaryActorTick.bCanEverTick = true; GetCapsuleComponent()->InitCapsuleSize(static_cast(Descent::MetresToUU(T::CapsuleRadiusM)), static_cast(Descent::MetresToUU(T::CapsuleHalfHeightM))); GetCapsuleComponent()->SetLightingChannels(true, true, false); bUseControllerRotationYaw = true; bUseControllerRotationPitch = false; bUseControllerRotationRoll = false; BaseEyeHeight = static_cast(Descent::MetresToUU(T::EyeHeightM - T::CapsuleHalfHeightM)); // The body (Player/Body): feet on the capsule bottom, no collision of its own; the mesh itself is given in BeginPlay. UDescentPlayerBodyComponent::ConfigureMeshDefaults(GetMesh(), Descent::MetresToUU(T::CapsuleHalfHeightM)); Camera = CreateDefaultSubobject(TEXT("Camera")); Camera->SetupAttachment(GetCapsuleComponent()); Camera->SetRelativeLocation(FVector(0.0, 0.0, BaseEyeHeight)); Camera->bUsePawnControlRotation = true; Camera->SetFieldOfView(T::FovDeg); Camera->bConstrainAspectRatio = false; Flashlight = CreateDefaultSubobject(TEXT("Flashlight")); Flashlight->SetupAttachment(Camera); Flashlight->SetMobility(EComponentMobility::Movable); Flashlight->bUseInverseSquaredFalloff = false; Flashlight->SetLightFalloffExponent(3.f); Flashlight->SetAttenuationRadius(static_cast(Descent::MetresToUU(T::FlashlightRadiusM))); Flashlight->SetInnerConeAngle(T::FlashlightInnerConeDeg); Flashlight->SetOuterConeAngle(T::FlashlightOuterConeDeg); Flashlight->SetLightColor(FLinearColor::FromSRGBColor(FColor(0xff, 0xf2, 0xdc))); Flashlight->SetIntensity(T::FlashlightIntensity * Descent::Tune::Carrier::LampBrightnessUnit); Flashlight->SetVolumetricScatteringIntensity(0.8f); Flashlight->SetLightingChannels(true, true, true); // channel 2: the first-person arm in dry air Flashlight->SetVisibility(false); ArmRoomFill = CreateDefaultSubobject(TEXT("ArmRoomFill")); ArmRoomFill->SetupAttachment(Camera); ArmRoomFill->SetRelativeLocation(Descent::MetresToUU(FVector(T::ArmFillBackM, T::ArmFillRightM, T::ArmFillUpM))); ArmRoomFill->SetMobility(EComponentMobility::Movable); ArmRoomFill->bUseInverseSquaredFalloff = false; ArmRoomFill->SetLightFalloffExponent(3.f); ArmRoomFill->SetAttenuationRadius(static_cast(Descent::MetresToUU(T::ArmFillRadiusM))); ArmRoomFill->SetCastShadows(false); ArmRoomFill->SetVolumetricScatteringIntensity(0.f); ArmRoomFill->SetLightingChannels(false, false, true); ArmRoomFill->SetIntensity(0.f); ArmRoomFill->SetVisibility(false); Interaction = CreateDefaultSubobject(TEXT("Interaction")); Vitals = CreateDefaultSubobject(TEXT("Vitals")); Arm = CreateDefaultSubobject(TEXT("Arm")); Arm->SetupAttachment(Camera); Body = CreateDefaultSubobject(TEXT("Body")); } void ADescentCharacter::BeginPlay() { Super::BeginPlay(); // The right arm at the right shoulder of the view, drawn only in this character's own view (Docs/status/arm.md). Arm->MountOnCamera(Camera, EDescentArmSide::Right); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { // Section 3.1: the carrier moves in sim step 2, before this actor's (and so its movement component's) tick. PrimaryActorTick.AddPrerequisite(Sim, Sim->GetSimTickFunction()); } } FVector ADescentCharacter::GetFeetWorldUU() const { return GetActorLocation() - FVector(0.0, 0.0, GetCapsuleComponent()->GetScaledCapsuleHalfHeight()); } FVector ADescentCharacter::GetFeetLocalM() const { const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); return C ? C->ToLocalM(GetFeetWorldUU()) : FVector::ZeroVector; } double ADescentCharacter::GetViewYawLocalDeg() const { const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); return FRotator::NormalizeAxis(GetControlRotation().Yaw - (C ? C->GetActorRotation().Yaw : 0.0)); } FVector ADescentCharacter::EyeWorldUU() const { return Camera->GetComponentLocation(); } FVector ADescentCharacter::EyeDirection() const { return Camera->GetForwardVector(); } void ADescentCharacter::SetViewLocal(double YawLocalDeg, double PitchDeg) { const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); const double CarrierYaw = C ? C->GetActorRotation().Yaw : 0.0; if (AController* PC = GetController()) { PC->SetControlRotation(FRotator(FMath::Clamp(PitchDeg, -T::PitchLimitDeg, T::PitchLimitDeg), CarrierYaw + YawLocalDeg, 0.0)); } } void ADescentCharacter::PlaceOnCarrier(const FVector& FeetLocalM, double YawLocalDeg, double PitchDeg) { ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (!C) { return; } StopAutoWalk(); SetSeatedIn(nullptr); SetFrozen(false); if (ADescentPlayerController* DPC = Cast(GetController())) { DPC->OnPlacedOnFoot(); } const double HalfH = GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); // a hair above the floor: the first tick lands on it and finds the base const FVector WorldUU = C->ToWorldUU(FeetLocalM + FVector(0.0, 0.0, 0.03)) + FVector(0.0, 0.0, HalfH); const double CarrierYaw = C->GetActorRotation().Yaw; UCharacterMovementComponent* Move = GetCharacterMovement(); if (Move) { Move->StopMovementImmediately(); Move->SetMovementMode(MOVE_Falling); } TeleportTo(WorldUU, FRotator(0.0, CarrierYaw + YawLocalDeg, 0.0), false, true); SetViewLocal(YawLocalDeg, PitchDeg); if (Move) { Move->Velocity = FVector::ZeroVector; } } void ADescentCharacter::SetFlashlight(bool bOn) { Flashlight->SetVisibility(bOn); } bool ADescentCharacter::IsFlashlightOn() const { return Flashlight->IsVisible(); } void ADescentCharacter::UseFocus() { const ADescentPlayerController* PC = Cast(GetController()); if (PC && PC->GetMode() != EDescentMode::Foot) { return; } // the body pushes a button with the right hand when there is something to use (a jack-in or a held state takes over by itself) if (Body && Interaction->GetFocus()) { Body->NotifyUse(); } Interaction->UseFocus(this); } bool ADescentCharacter::StartAutoWalk(const TArray& Way, const FString& Label) { if (Way.Num() == 0) { return false; } AutoWalk = FDescentAutoWalk(); AutoWalk.bActive = true; AutoWalk.Waypoints = Way; AutoWalk.SecondsLeft = T::MoveToTimeoutSec; AutoWalk.Label = Label; return true; } void ADescentCharacter::SteerAutoWalk(double Dt) { UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld()); const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (!AutoWalk.bActive || !Sim || !C) { return; } const FVector Feet = GetFeetLocalM(); const bool bFinal = AutoWalk.Index >= AutoWalk.Waypoints.Num() - 1; const FVector2D Target = AutoWalk.Waypoints[AutoWalk.Index]; const FVector2D ToTarget = Target - FVector2D(Feet.X, Feet.Y); const double Dist = ToTarget.Size(); if (Dist < (bFinal ? T::MoveToArriveM : T::MoveToWaypointM)) { if (bFinal) { AutoWalk = FDescentAutoWalk(); // arrived: stop pushing at once Sim->ClearDebugIntent(); return; } ++AutoWalk.Index; return; } AutoWalk.SecondsLeft -= Dt; if (AutoWalk.SecondsLeft <= 0.0) { const FString Label = AutoWalk.Label; AutoWalk = FDescentAutoWalk(); Sim->ClearDebugIntent(); Descent::Cmd::Fail(GetWorld(), TEXT("Descent.Player.MoveTo"), FString::Printf(TEXT("timed out after %.0f s walking to %s; stopped at carrier-local (%.2f, %.2f, %.2f)"), T::MoveToTimeoutSec, *Label, Feet.X, Feet.Y, Feet.Z)); return; } // heading of the target in the world (yaw only) against the view yaw const FVector DirWorld = C->GetActorTransform().TransformVectorNoScale(FVector(ToTarget.X, ToTarget.Y, 0.0)); const double TargetYaw = FMath::RadiansToDegrees(FMath::Atan2(DirWorld.Y, DirWorld.X)); const double Err = FRotator::NormalizeAxis(TargetYaw - GetControlRotation().Yaw); const double Pitch = FRotator::NormalizeAxis(GetControlRotation().Pitch); FDescentIntent I; I.Look.X = FMath::Clamp(Err * T::MoveToTurnGain, -T::MoveToMaxTurnDps, T::MoveToMaxTurnDps); I.Look.Y = FMath::Clamp(-Pitch * 6.0, -120.0, 120.0); I.Move.X = FMath::Abs(Err) < 25.0 ? ((Dist < 0.9 && bFinal) ? 0.5 : 1.0) : 0.0; Sim->SetDebugIntent(I, 0.1); } void ADescentCharacter::ApplyIntent(double Dt) { UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld()); AController* PC = GetController(); if (!Sim || !PC) { return; } const ADescentPlayerController* DPC = Cast(PC); const bool bFoot = !DPC || DPC->GetMode() == EDescentMode::Foot; const bool bSwim = DPC && DPC->GetMode() == EDescentMode::Swim; FDescentIntent I = Sim->GetIntent(); if (!(bFoot || bSwim) || bFrozen) { I = FDescentIntent(); } // look: a rate in degrees per second FRotator R = PC->GetControlRotation(); R.Yaw = FRotator::NormalizeAxis(R.Yaw + I.Look.X * Dt); R.Pitch = FMath::Clamp(FRotator::NormalizeAxis(R.Pitch) + I.Look.Y * Dt, -T::PitchLimitDeg, T::PitchLimitDeg); R.Roll = 0.0; PC->SetControlRotation(R); // The body turns with the view in the SAME frame. Without this the actor took the yaw one frame later (the controller turns its // pawn before this tick): measured 2.3 degrees behind the camera at 70 degrees per second, which shows as the body dragging and // jittering against the view whenever the mouse moves. if (!bSeated && bUseControllerRotationYaw) { FaceRotation(R, static_cast(Dt)); } // move: relative to the view yaw UCharacterMovementComponent* Move = GetCharacterMovement(); if (bSwim) { // Section 5.3: wish from the full view rotation plus Space / C on world up; the movement component does the rest. if (UDescentCharacterMovement* Swim = Cast(Move)) { Swim->SwimWish = (R.RotateVector(FVector(I.Move.X, I.Move.Y, 0.0)) + FVector(0.0, 0.0, I.Move.Z)).GetClampedToMaxSize(1.0); Swim->bSwimSprint = I.bSprint; } bJumpHeld = I.bJump; Camera->SetRelativeLocation(FVector(0.0, 0.0, BaseEyeHeight)); return; } // carrying an ore box: 2.3 m/s, no sprint (section 5.5) const UDescentCargoSubsystem* CarryCargo = UDescentCargoSubsystem::Get(GetWorld()); const bool bCarrying = CarryCargo && CarryCargo->Carried(); const double Speed = bCarrying ? T::CarrySpeedMps : (I.bSprint ? T::SprintSpeedMps : T::WalkSpeedMps); Move->MaxWalkSpeed = static_cast(Descent::MetresToUU(Speed)); const FVector Wish = FRotator(0.0, R.Yaw, 0.0).RotateVector(FVector(I.Move.X, I.Move.Y, 0.0)); if (!Wish.IsNearlyZero()) { AddMovementInput(Wish.GetClampedToMaxSize(1.0), 1.0f); } if (I.bJump && !bJumpHeld && bFoot) { Jump(); } bJumpHeld = I.bJump; // View bob (2026-10-12; measured before: 8.7 cm peak to peak walking, 12.5 cm running, up to 4.3 cm in one frame): up and down // only, about 1 cm walking and 1.5 cm running, following the pelvis of the body's locomotion clip one frame ahead (the pose drawn // this frame is stepped after this), so it is in step with the footfalls. Without a measured clip: by distance walked, as before. const double Planar = GetVelocity().Size2D() / 100.0; BobAmount = Descent::Damp(BobAmount, Move->IsFalling() ? 0.0 : Descent::Clamp(Planar, 0.0, 1.0), 8.0, Dt); if (!Move->IsFalling()) { BobPhase += Planar * Dt * T::BobPerMetre; } double Signal = 0.0, FootfallHz = 0.0; if (!Body || !Body->GetViewBobSignal(Dt, Signal, &FootfallHz)) { Signal = FMath::Sin(2.0 * BobPhase) * BobAmount; } ViewBobCm = Signal * ViewBobAmplitudeCm(Planar, FootfallHz) * ViewBobStrength(); Camera->SetRelativeLocation(FVector(0.0, 0.0, BaseEyeHeight + ViewBobCm)); } namespace { TAutoConsoleVariable CVarViewBob(TEXT("descent.View.Bob"), static_cast(Descent::Tune::Player::ViewBobDefault), TEXT("Strength of the view bob while walking and running. 0 = none, 1 = default (about 1 cm walking, 1.5 cm running, peak to peak).")); } double ADescentCharacter::ViewBobStrength() { return FMath::Clamp(static_cast(CVarViewBob.GetValueOnGameThread()), 0.0, 4.0); } double ADescentCharacter::ViewBobAmplitudeCm(double PlanarMps, double FootfallHz) { const double Run = Descent::Clamp((PlanarMps - T::WalkSpeedMps) / (T::SprintSpeedMps - T::WalkSpeedMps), 0.0, 1.0); double Amp = PlanarMps <= T::WalkSpeedMps ? T::ViewBobWalkCm * Descent::Clamp(PlanarMps / T::WalkSpeedMps, 0.0, 1.0) : FMath::Lerp(T::ViewBobWalkCm, T::ViewBobRunCm, Run); // The bob follows the footfalls, and since the play rate is matched to the ground the walk's footfalls come faster (3.9 a // second, 2.9 before). So the camera's SPEED is held at what it was: a quicker bob is a smaller one (walking 0.66 cm peak to peak). if (FootfallHz > 0.0) { Amp = FMath::Min(Amp, FMath::Lerp(T::ViewBobWalkMaxCmS, T::ViewBobRunMaxCmS, Run) / (2.0 * UE_DOUBLE_PI * FootfallHz)); } return Amp; } void ADescentCharacter::RescueIfLost() { const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (!C) { return; } // Only a fall out of the hull is rescued (below the keel inside the hull's footprint). A deliberate teleport far away (debug commands, // the world track's tours) is left alone: outside the hull the character hovers (see Tick). const FVector Local = GetFeetLocalM(); if (GetCharacterMovement()->IsFalling() && Local.Z < T::RescueBelowZM && FMath::Abs(Local.X) < 62.0 && FMath::Abs(Local.Y) < 16.0) { ++RescueCount; UE_LOG(LogDescent, Warning, TEXT("Player: fell out of the carrier (local %.1f %.1f %.1f); put back on the bridge"), Local.X, Local.Y, Local.Z); if (const Descent::Carrier::FRoomDef* Bridge = Descent::Carrier::FLayout::Get().FindRoom(TEXT("bridge"))) { PlaceOnCarrier(FVector(Bridge->Stop.X, Bridge->Stop.Y, Bridge->FloorZ), Bridge->StopYawDeg); } } } bool ADescentCharacter::TeleportTo(const FVector& DestLocation, const FRotator& DestRotation, bool bIsATest, bool bNoCheck) { const bool bOk = Super::TeleportTo(DestLocation, DestRotation, bIsATest, bNoCheck); if (bOk && !bIsATest && !bSeated) { ADescentPlayerController* DPC = Cast(GetController()); const ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (DPC && K && DPC->GetMode() == EDescentMode::Foot && !K->IsDry(GetFeetLocalM() + FVector(0.0, 0.0, T::SwimEnterBelowSurfaceM))) { GetCharacterMovement()->Velocity = FVector::ZeroVector; DPC->EnterSwim(); } } return bOk; } void ADescentCharacter::StartSwimming(const FVector* WorldUU) { StopAutoWalk(); SetSeatedIn(nullptr); SetFrozen(false); if (WorldUU) { TeleportTo(*WorldUU, GetActorRotation(), false, true); GetCharacterMovement()->Velocity = FVector::ZeroVector; } if (UDescentCharacterMovement* Swim = Cast(GetCharacterMovement())) { Swim->StartSwim(); } } bool ADescentCharacter::IsSwimming() const { const UDescentCharacterMovement* Swim = Cast(GetCharacterMovement()); return Swim && Swim->IsSwimMode(); } void ADescentCharacter::SetSeatedIn(AKazahTender* Tender) { if (Tender) { StopAutoWalk(); bSeated = true; bInTender = true; GetCharacterMovement()->StopMovementImmediately(); GetCharacterMovement()->DisableMovement(); SetActorEnableCollision(false); AttachToActor(Tender, FAttachmentTransformRules::KeepWorldTransform); namespace TT = Descent::Tune::Tender; SetActorRelativeLocation(Descent::MetresToUU(FVector(TT::EyeX - 0.25, TT::EyeY, TT::EyeZ - 0.55))); SetActorRelativeRotation(FRotator::ZeroRotator); Flashlight->SetVisibility(false); } else if (bSeated) { bSeated = false; bInTender = false; DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); SetActorRotation(FRotator(0.0, GetActorRotation().Yaw, 0.0)); SetActorEnableCollision(true); GetCharacterMovement()->SetMovementMode(MOVE_Falling); } } void ADescentCharacter::SetHeldBy(AActor* Parent) { if (!Parent) { SetSeatedIn(nullptr); return; } StopAutoWalk(); bSeated = true; GetCharacterMovement()->StopMovementImmediately(); GetCharacterMovement()->DisableMovement(); AttachToActor(Parent, FAttachmentTransformRules::KeepWorldTransform); } void ADescentCharacter::StartJackVisual(const FVector& PortWorldUU, const FVector& PlugDirectionWorld) { if (Arm && UDescentPlayerBodyComponent::IsOldArmView()) { Arm->StartJackIn(PortWorldUU, PlugDirectionWorld); } else if (Body) { Body->StartJack(&PortWorldUU); // the body's right arm reaches for the port (the retargeted Touch_R, the hand taken to the port) } } void ADescentCharacter::SetFrozen(bool bFreeze) { if (bFrozen == bFreeze) { return; } bFrozen = bFreeze; UCharacterMovementComponent* Mv = GetCharacterMovement(); if (bFreeze) { Mv->StopMovementImmediately(); Mv->DisableMovement(); } else if (!bSeated && Mv->MovementMode == MOVE_None) { Mv->SetMovementMode(MOVE_Falling); } } void ADescentCharacter::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); ADescentPlayerController* DPC = Cast(GetController()); // the arm's room light: the colour and level of the lamps of the room the player stands in { const ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld()); const Descent::Carrier::FRoomDef* Room = (K && K->Blockout && !bSeated && K->IsDry(K->ToLocalM(Camera->GetComponentLocation()))) ? Descent::Carrier::FLayout::Get().FindRoom(K->Blockout->GetViewerRoom()) : nullptr; if (Room) { ArmRoomFill->SetLightColor(Room->LampColor); ArmRoomFill->SetIntensity(static_cast(Room->LampIntensity * Descent::Tune::Carrier::LampBrightnessUnit * T::ArmFillOfLamp)); } ArmRoomFill->SetVisibility(Room != nullptr); } // The old first-person arm: only while it is the view (descent.Body.OldArm 1; the Twinblast body is the view otherwise and the arm // stays Hidden). Then it is up on foot and swimming and away in every other mode (a Tender has its own view; title, ladder, death). // Only Idle and Hidden are decided here: a pose something else asked for (grip, tool, a reach, the jack-in) is left alone while up. if (Arm && !Arm->IsJackActive()) { const bool bUp = UDescentPlayerBodyComponent::IsOldArmView() && DPC && !bSeated && (DPC->GetMode() == EDescentMode::Foot || DPC->GetMode() == EDescentMode::Swim); if (!bUp && (Arm->GetNamedPose() != EDescentArmPose::Hidden || Arm->IsCustomPose())) { Arm->SetPose(EDescentArmPose::Hidden); } else if (bUp && Arm->GetNamedPose() == EDescentArmPose::Hidden && !Arm->IsCustomPose()) { Arm->SetPose(EDescentArmPose::Idle); } } if (bSeated || bFrozen) { ApplyIntent(DeltaSeconds); // zeroed inside: only keeps the look state tidy return; } // On foot is for the dry inside of the hull (section 5.2); anywhere in water the body swims (5.3). The pool is entered through the // gap in its rail: once the feet are 0.2 m under the surface the mode becomes Swim. if (const ADescentCarrier* K = UDescentCarrierSubsystem::GetCarrier(GetWorld())) { UCharacterMovementComponent* Mv = GetCharacterMovement(); const FVector FeetLocal = GetFeetLocalM(); const bool bDry = K->IsDry(FeetLocal + FVector(0.0, 0.0, T::SwimEnterBelowSurfaceM)); if (DPC && DPC->GetMode() == EDescentMode::Foot && !bDry && !IsSwimming()) { DPC->EnterSwim(); } else if (!DPC && !bDry && Mv->IsFalling()) { // not possessed (a scenario flies a fly pawn): the body waits where it is instead of falling forever Mv->SetMovementMode(MOVE_Flying); Mv->Velocity = FVector::ZeroVector; } else if (bDry && Mv->MovementMode == MOVE_Flying) { Mv->SetMovementMode(MOVE_Falling); } } SteerAutoWalk(DeltaSeconds); ApplyIntent(DeltaSeconds); RescueIfLost(); } DESCENT_REGISTER_GAME_CLASS(Pawn, 100, ADescentCharacter)