#include "Runtime/World/CPlayer.hpp"

#include <algorithm>
#include <array>
#include <cmath>
#include <string>

#include "Runtime/CDependencyGroup.hpp"
#include "Runtime/CGameState.hpp"
#include "Runtime/CSimplePool.hpp"
#include "Runtime/CStateManager.hpp"
#include "Runtime/Audio/CStreamAudioManager.hpp"
#include "Runtime/Camera/CBallCamera.hpp"
#include "Runtime/Camera/CCinematicCamera.hpp"
#include "Runtime/Camera/CFirstPersonCamera.hpp"
#include "Runtime/Character/CSteeringBehaviors.hpp"
#include "Runtime/Collision/CGameCollision.hpp"
#include "Runtime/Collision/CMetroidAreaCollider.hpp"
#include "Runtime/GameGlobalObjects.hpp"
#include "Runtime/Input/ControlMapper.hpp"
#include "Runtime/MP1/CSamusHud.hpp"
#include "Runtime/MP1/World/CMetroidBeta.hpp"
#include "Runtime/MP1/World/CThardusRockProjectile.hpp"
#include "Runtime/Weapon/CEnergyProjectile.hpp"
#include "Runtime/World/CActorParameters.hpp"
#include "Runtime/World/CHUDBillboardEffect.hpp"
#include "Runtime/World/CPatterned.hpp"
#include "Runtime/World/CScriptAreaAttributes.hpp"
#include "Runtime/World/CScriptGrapplePoint.hpp"
#include "Runtime/World/CScriptPlayerHint.hpp"
#include "Runtime/World/CScriptWater.hpp"

#include <logvisor/logvisor.hpp>

#include "TCastTo.hpp" // Generated file, do not modify include path

namespace metaforce {
namespace {
logvisor::Module Log("metaforce::CPlayer");

constexpr CMaterialFilter SolidMaterialFilter = CMaterialFilter::MakeInclude(CMaterialList(EMaterialTypes::Solid));

constexpr CMaterialFilter LineOfSightFilter = CMaterialFilter::MakeIncludeExclude(
    {EMaterialTypes::Solid},
    {EMaterialTypes::ProjectilePassthrough, EMaterialTypes::ScanPassthrough, EMaterialTypes::Player});

constexpr CMaterialFilter OccluderFilter = CMaterialFilter::MakeIncludeExclude(
    {EMaterialTypes::Solid, EMaterialTypes::Occluder},
    {EMaterialTypes::ProjectilePassthrough, EMaterialTypes::ScanPassthrough, EMaterialTypes::Player});

constexpr CMaterialFilter BallTransitionCollide = CMaterialFilter::MakeIncludeExclude(
    {EMaterialTypes::Solid}, {EMaterialTypes::ProjectilePassthrough, EMaterialTypes::Player, EMaterialTypes::Character,
                              EMaterialTypes::CameraPassthrough});

constexpr std::array<float, 8> skStrafeDistances{
    11.8f, 11.8f, 11.8f, 5.0f, 6.0f, 5.0f, 5.0f, 6.0f,
};

constexpr std::array<float, 8> skDashStrafeDistances{
    11.8f, 30.0f, 22.6f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f,
};

constexpr std::array<float, 8> skOrbitForwardDistances{
    11.8f, 11.8f, 11.8f, 5.0f, 6.0f, 5.0f, 5.0f, 6.0f,
};

constexpr std::array<const char*, 2> UnlockMessageResBases{
    "STRG_SlideShow_Unlock1_",
    "STRG_SlideShow_Unlock2_",
};

constexpr std::array<u16, 24> skPlayerLandSfxSoft{
    0xFFFF,
    SFXsam_landstone_00,
    SFXsam_landmetl_00,
    SFXsam_landgrass_00,
    SFXsam_landice_00,
    0xFFFF,
    SFXsam_landgrate_00,
    SFXsam_landphazon_00,
    SFXsam_landdirt_00,
    SFXlav_landlava_00,
    SFXsam_landlavastone_00,
    SFXsam_landsnow_00,
    SFXsam_landmud_00,
    0xFFFF,
    SFXsam_landgrass_00,
    SFXsam_landmetl_00,
    SFXsam_landmetl_00,
    SFXsam_landdirt_00,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    SFXsam_landwood_00,
    SFXsam_b_landorg_00,
};

constexpr std::array<u16, 24> skPlayerLandSfxHard{
    0xFFFF,
    SFXsam_landstone_02,
    SFXsam_b_landmetl_02,
    SFXsam_landgrass_02,
    SFXsam_landice_02,
    0xFFFF,
    SFXsam_landgrate_02,
    SFXsam_landphazon_02,
    SFXsam_landdirt_02,
    SFXlav_landlava_02,
    SFXsam_landlavastone_02,
    SFXsam_landsnow_02,
    SFXsam_landmud_02,
    0xFFFF,
    SFXsam_landgrass_02,
    SFXsam_b_landmetl_02,
    SFXsam_b_landmetl_02,
    SFXsam_landdirt_02,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    SFXsam_landwood_02,
    SFXsam_landorg_02,
};

constexpr std::array<u16, 24> skLeftStepSounds{
    0xFFFF,
    SFXsam_wlkstone_00,
    SFXsam_wlkmetal_00,
    SFXsam_b_wlkgrass_00,
    SFXsam_wlkice_00,
    0xFFFF,
    SFXsam_wlkgrate_00,
    SFXsam_wlkphazon_00,
    SFXsam_wlkdirt_00,
    SFXlav_wlklava_00,
    SFXsam_wlklavastone_00,
    SFXsam_wlksnow_00,
    SFXsam_wlkmud_00,
    0xFFFF,
    SFXsam_b_wlkorg_00,
    SFXsam_wlkmetal_00,
    SFXsam_wlkmetal_00,
    SFXsam_wlkdirt_00,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    SFXsam_wlkwood_00,
    SFXsam_b_wlkorg_00,
};

constexpr std::array<u16, 24> skRightStepSounds{
    0xFFFF,
    SFXsam_wlkstone_01,
    SFXsam_wlkmetal_01,
    SFXsam_b_wlkgrass_01,
    SFXsam_wlkice_01,
    0xFFFF,
    SFXsam_wlkgrate_01,
    SFXsam_wlkphazon_01,
    SFXsam_wlkdirt_01,
    SFXlav_wlklava_01,
    SFXsam_wlklavastone_01,
    SFXsam_wlksnow_01,
    SFXsam_wlkmud_01,
    0xFFFF,
    SFXsam_b_wlkorg_01,
    SFXsam_wlkmetal_01,
    SFXsam_wlkmetal_01,
    SFXsam_wlkdirt_01,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    0xFFFF,
    SFXsam_wlkwood_01,
    SFXsam_b_wlkorg_01,
};

constexpr float RCP_2PI = 0.15915494;

constexpr std::array<std::pair<CPlayerState::EItemType, ControlMapper::ECommands>, 4> skVisorToItemMapping{{
    {CPlayerState::EItemType::CombatVisor, ControlMapper::ECommands::NoVisor},
    {CPlayerState::EItemType::XRayVisor, ControlMapper::ECommands::XrayVisor},
    {CPlayerState::EItemType::ScanVisor, ControlMapper::ECommands::InviroVisor},
    {CPlayerState::EItemType::ThermalVisor, ControlMapper::ECommands::ThermoVisor},
}};

CModelData MakePlayerAnimRes(CAssetId resId, const zeus::CVector3f& scale) {
  return CModelData{CAnimRes(resId, 0, scale, 0, true), 1};
}

uint32_t GetOrbitScreenBoxHalfExtentXScaled(int zone) {
  return g_tweakPlayer->GetOrbitScreenBoxHalfExtentX(zone) * g_Viewport.x8_width / 640;
}

uint32_t GetOrbitScreenBoxHalfExtentYScaled(int zone) {
  return g_tweakPlayer->GetOrbitScreenBoxHalfExtentY(zone) * g_Viewport.xc_height / 448;
}

uint32_t GetOrbitScreenBoxCenterXScaled(int zone) {
  return g_tweakPlayer->GetOrbitScreenBoxCenterX(zone) * g_Viewport.x8_width / 640;
}

uint32_t GetOrbitScreenBoxCenterYScaled(int zone) {
  return g_tweakPlayer->GetOrbitScreenBoxCenterY(zone) * g_Viewport.xc_height / 448;
}

uint32_t GetOrbitZoneIdealXScaled(int zone) {
  return g_tweakPlayer->GetOrbitZoneIdealX(zone) * g_Viewport.x8_width / 640;
}

uint32_t GetOrbitZoneIdealYScaled(int zone) {
  return g_tweakPlayer->GetOrbitZoneIdealY(zone) * g_Viewport.xc_height / 448;
}
} // Anonymous namespace

CPlayer::CPlayer(TUniqueId uid, const zeus::CTransform& xf, const zeus::CAABox& aabb, CAssetId resId,
                 const zeus::CVector3f& playerScale, float mass, float stepUp, float stepDown, float ballRadius,
                 const CMaterialList& ml)
: CPhysicsActor(uid, true, "CPlayer", CEntityInfo(kInvalidAreaId, CEntity::NullConnectionList), xf,
                MakePlayerAnimRes(resId, playerScale), ml, aabb, SMoverData(mass), CActorParameters::None(), stepUp,
                stepDown)
, x2d8_fpBounds(aabb)
, x7d0_animRes(resId, 0, playerScale, 0, true)
, x7d8_beamScale(playerScale) {
  x490_gun = std::make_unique<CPlayerGun>(uid);
  x49c_gunHolsterRemTime = g_tweakPlayerGun->GetGunNotFiringTime();
  x4a0_failsafeTest = std::make_unique<CFailsafeTest>();
  x76c_cameraBob =
      std::make_unique<CPlayerCameraBob>(CPlayerCameraBob::ECameraBobType::One, CPlayerCameraBob::GetCameraBobExtent(),
                                         CPlayerCameraBob::GetCameraBobPeriod());
  const CAssetId beamId = g_tweakPlayerRes->GetBeamBallTransitionModel(x7ec_beam);
  x7f0_ballTransitionBeamModel = std::make_unique<CModelData>(CStaticRes(beamId, playerScale));
  x730_transitionModels.reserve(3);
  x768_morphball = std::make_unique<CMorphBall>(*this, ballRadius);

  SetInertiaTensorScalar(xe8_mass);
  x1f4_lastNonCollidingState = GetMotionState();
  x490_gun->SetTransform(x34_transform);
  x490_gun->GetGrappleArm().SetTransform(x34_transform);

  InitializeBallTransition();
  const zeus::CAABox ballTransAABB = x64_modelData->GetBounds();
  x2f0_ballTransHeight = ballTransAABB.max.z() - ballTransAABB.min.z();

  SetCalculateLighting(true);

  x90_actorLights->SetCastShadows(true);
  x50c_moveDir.z() = 0.f;
  if (x50c_moveDir.canBeNormalized()) {
    x50c_moveDir.normalize();
  }
  x2b4_accelerationTable.push_back(20.f);
  x2b4_accelerationTable.push_back(80.f);
  x2b4_accelerationTable.push_back(80.f);
  x2b4_accelerationTable.push_back(270.f);
  SetMaximumCollisionVelocity(25.f);
  x354_onScreenOrbitObjects.reserve(64);
  x344_nearbyOrbitObjects.reserve(64);
  x364_offScreenOrbitObjects.reserve(64);
  x64_modelData->SetScale(playerScale);
  x7f0_ballTransitionBeamModel->SetScale(playerScale);
  LoadAnimationTokens();

  _CreateReflectionCube();
}

void CPlayer::InitializeBallTransition() {
  if (x64_modelData && x64_modelData->HasAnimData()) {
    x64_modelData->GetAnimationData()->SetAnimation(CAnimPlaybackParms(2, -1, 1.f, true), false);
  }
}

bool CPlayer::IsTransparent() const { return x588_alpha < 1.f; }

float CPlayer::GetTransitionAlpha(const zeus::CVector3f& camPos, float zNear) const {
  const float zLimit = (x2d8_fpBounds.max.x() - x2d8_fpBounds.min.x()) * 0.5f + zNear;
  const float zStart = 1.f + zLimit;
  const float dist = (camPos - GetEyePosition()).magnitude();

  if (dist >= zLimit && dist <= zStart) {
    return (dist - zLimit) / (zStart - zLimit);
  }

  if (dist > zStart) {
    return 1.f;
  }

  return 0.f;
}

s32 CPlayer::ChooseTransitionToAnimation(float dt, CStateManager& mgr) const {
  if (x258_movementState == EPlayerMovementState::ApplyJump) {
    return 3; // B_airposetoball_samus
  }

  const zeus::CVector3f localVel = x34_transform.transposeRotate(x138_velocity);
  zeus::CVector3f localVelFlat = localVel;
  localVelFlat.z() = 0.f;
  const float localVelFlatMag = localVelFlat.magnitude();
  if (localVelFlatMag <= 1.f) {
    return 2; // B_readytoball_samus
  }

  float velAng = std::atan2(-localVelFlat.x(), localVelFlat.y());
  constexpr float twoPi = zeus::degToRad(360.f);
  if (velAng > twoPi) {
    velAng = velAng - static_cast<int>(velAng * RCP_2PI) * twoPi;
  } else if (velAng < 0.f) {
    velAng = twoPi + (velAng - static_cast<int>(velAng * RCP_2PI) * twoPi);
  }
  const float velDeg = zeus::radToDeg(velAng);
  if (velDeg >= 45.f && velDeg <= 315.f) {
    return 1; // B_readytostationarybackwards_samus
  }

  if (localVelFlatMag < 0.5f * GetActualFirstPersonMaxVelocity(dt)) {
    return 0; // B_forwardtoballforward_samus
  }

  return 4; // B_runtoballfoward_samus
}

void CPlayer::TransitionToMorphBallState(float dt, CStateManager& mgr) {
  x584_ballTransitionAnim = ChooseTransitionToAnimation(dt, mgr);
  x58c_transitionVel = x138_velocity.magnitude();
  if (x64_modelData && x64_modelData->HasAnimData()) {
    const CAnimPlaybackParms parms(x584_ballTransitionAnim, -1, 1.f, true);
    x64_modelData->GetAnimationData()->SetAnimation(parms, false);
    x64_modelData->GetAnimationData()->SetAnimDir(CAnimData::EAnimDir::Forward);
  }
  x64_modelData->EnableLooping(false);
  x64_modelData->Touch(mgr, 0);
  x150_momentum = zeus::skZero3f;
  CPhysicsActor::Stop();
  SetMorphBallState(EPlayerMorphBallState::Morphing, mgr);
  SetCameraState(EPlayerCameraState::Transitioning, mgr);
  x500_lookDir = x34_transform.basis[1];
  x50c_moveDir = x500_lookDir;
  x50c_moveDir.z() = 0.f;
  if (x50c_moveDir.canBeNormalized()) {
    x50c_moveDir.normalize();
  } else {
    x500_lookDir = zeus::skForward;
    x50c_moveDir = zeus::skForward;
  }
  CBallCamera* ballCam = mgr.GetCameraManager()->GetBallCamera();
  mgr.GetCameraManager()->SetPlayerCamera(mgr, ballCam->GetUniqueId());
  if (!mgr.GetCameraManager()->HasBallCameraInitialPositionHint(mgr)) {
    mgr.GetCameraManager()->SetupBallCamera(mgr);
    ballCam->SetState(CBallCamera::EBallCameraState::ToBall, mgr);
  } else {
    ballCam->SetState(CBallCamera::EBallCameraState::Default, mgr);
    SetCameraState(EPlayerCameraState::Ball, mgr);
    const zeus::CTransform newXf = mgr.GetCameraManager()->GetFirstPersonCamera()->GetTransform();
    ballCam->SetTransform(newXf);
    ballCam->TeleportCamera(newXf.origin, mgr);
    mgr.GetCameraManager()->SetupBallCamera(mgr);
    ballCam->SetFovInterpolation(mgr.GetCameraManager()->GetFirstPersonCamera()->GetFov(),
                                 CCameraManager::ThirdPersonFOV(), 1.f, 0.f);
  }
  SetOrbitRequest(EPlayerOrbitRequest::EnterMorphBall, mgr);
  x490_gun->CancelFiring(mgr);
  HolsterGun(mgr);
}

void CPlayer::TransitionFromMorphBallState(CStateManager& mgr) {
  x584_ballTransitionAnim = 14; // B_ball_unfurl
  x58c_transitionVel = zeus::CVector2f(x138_velocity.x(), x138_velocity.y()).magnitude();
  if (x58c_transitionVel < 1.f) {
    x584_ballTransitionAnim = 5; // ballstationarytoready_random
  }

  if (x258_movementState != EPlayerMovementState::OnGround) {
    const zeus::CVector3f ballPos = GetBallPosition();
    if (mgr.RayCollideWorld(ballPos, ballPos + zeus::CVector3f(0.f, 0.f, -7.f), BallTransitionCollide, this)) {
      x584_ballTransitionAnim = 7; // B_balljumptoairpose
    }
  }

  if (x64_modelData && x64_modelData->HasAnimData()) {
    const CAnimPlaybackParms parms(x584_ballTransitionAnim, -1, 1.f, true);
    x64_modelData->GetAnimationData()->SetAnimation(parms, false);
    x64_modelData->GetAnimationData()->SetAnimDir(CAnimData::EAnimDir::Forward);
  }

  x64_modelData->EnableLooping(false);
  x64_modelData->Touch(mgr, 0);
  SetMorphBallState(EPlayerMorphBallState::Unmorphing, mgr);
  x768_morphball->LeaveMorphBallState(mgr);
  mgr.GetCameraManager()->SetPlayerCamera(mgr, mgr.GetCameraManager()->GetFirstPersonCamera()->GetUniqueId());

  zeus::CVector3f camToPlayer = GetTranslation() - mgr.GetCameraManager()->GetBallCamera()->GetTranslation();
  if (camToPlayer.canBeNormalized()) {
    camToPlayer.normalize();
    zeus::CVector3f vecFlat = x500_lookDir;
    vecFlat.z() = 0.f;
    zeus::CVector3f f31 = vecFlat.canBeNormalized() && vecFlat.magnitude() >= 0.1f ? x518_leaveMorphDir : camToPlayer;
    if (x9c6_26_outOfBallLookAtHint) {
      if (const TCastToConstPtr<CScriptPlayerHint> hint = mgr.GetObjectById(x830_playerHint)) {
        zeus::CVector3f deltaFlat = hint->GetTranslation() - GetTranslation();
        deltaFlat.z() = 0.f;
        if (deltaFlat.canBeNormalized()) {
          f31 = deltaFlat.normalized();
        }
      }
    }
    if (x9c7_25_outOfBallLookAtHintActor) {
      if (const TCastToConstPtr<CScriptPlayerHint> hint = mgr.GetObjectById(x830_playerHint)) {
        if (const TCastToConstPtr<CActor> act = mgr.GetObjectById(hint->GetActorId())) {
          zeus::CVector3f deltaFlat = act->GetOrbitPosition(mgr) - GetTranslation();
          deltaFlat.z() = 0.f;
          if (deltaFlat.canBeNormalized()) {
            f31 = deltaFlat.normalized();
          }
        }
      }
    }

    if (std::acos(zeus::clamp(-1.f, camToPlayer.dot(f31), 1.f)) < M_PIF / 1.2f || x9c7_25_outOfBallLookAtHintActor) {
      SetTransform(zeus::lookAt(GetTranslation(), GetTranslation() + f31));
    } else {
      SetTransform(zeus::lookAt(GetTranslation(), GetTranslation() + camToPlayer));
      UpdateArmAndGunTransforms(0.01f, mgr);
    }
  } else {
    SetTransform(CreateTransformFromMovementDirection());
  }

  CBallCamera* ballCam = mgr.GetCameraManager()->GetBallCamera();
  if (const TCastToConstPtr<CActor> act = mgr.GetObjectById(ballCam->GetTooCloseActorId())) {
    if (ballCam->GetTooCloseActorDistance() < 20.f && ballCam->GetTooCloseActorDistance() > 1.f) {
      zeus::CVector3f deltaFlat = act->GetTranslation() - GetTranslation();
      deltaFlat.z() = 0.f;
      zeus::CVector3f deltaFlat2 = act->GetTranslation() - ballCam->GetTranslation();
      deltaFlat2.z() = 0.f;
      if (deltaFlat.canBeNormalized() && deltaFlat2.canBeNormalized()) {
        deltaFlat.normalize();
        zeus::CVector3f camLookFlat = ballCam->GetTransform().basis[1];
        camLookFlat.z() = 0.f;
        camLookFlat.normalize();
        deltaFlat2.normalize();
        if (deltaFlat.dot(deltaFlat2) >= 0.3f && deltaFlat2.dot(camLookFlat) >= 0.7f) {
          SetTransform(zeus::lookAt(GetTranslation(), GetTranslation() + deltaFlat));
        }
      }
    }
  }

  ForceGunOrientation(x34_transform, mgr);
  DrawGun(mgr);
  ballCam->SetState(CBallCamera::EBallCameraState::FromBall, mgr);
  ClearForcesAndTorques();
  SetAngularVelocityWR(zeus::CAxisAngle());
  AddMaterial(EMaterialTypes::GroundCollider, mgr);
  x150_momentum = zeus::skZero3f;
  SetCameraState(EPlayerCameraState::Transitioning, mgr);
  x824_transitionFilterTimer = 0.01f;
  x57c_ = 0;
  x580_ = 0;
  if (!ballCam->TransitionFromMorphBallState(mgr)) {
    x824_transitionFilterTimer = 0.95f;
    LeaveMorphBallState(mgr);
  }
}

s32 CPlayer::GetNextBallTransitionAnim(float dt, bool& loopOut, CStateManager& mgr) {
  loopOut = false;

  const zeus::CVector2f vel(x138_velocity.x(), x138_velocity.y());
  if (!vel.canBeNormalized()) {
    return 12; // B_ball_ready_samus
  }

  const float velMag = vel.magnitude();
  const float maxVel = GetActualFirstPersonMaxVelocity(dt);
  if (velMag <= 0.2f * maxVel) {
    return 12; // B_ball_ready_samus
  }

  loopOut = true;

  const s32 ret = velMag >= maxVel ? 13 : 15; // B_ball_runloop_samus : B_ball_walkloop_samus
  if (x50c_moveDir.dot(mgr.GetCameraManager()->GetBallCamera()->GetTransform().basis[1]) < -0.5f) {
    return 12; // B_ball_ready_samus
  }

  return ret;
}

void CPlayer::UpdateMorphBallTransition(float dt, CStateManager& mgr) {
  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Unmorphed:
  case EPlayerMorphBallState::Morphed: {
    CPlayerState::EPlayerSuit suit = mgr.GetPlayerState()->GetCurrentSuitRaw();
    if (mgr.GetPlayerState()->IsFusionEnabled()) {
      suit = CPlayerState::EPlayerSuit(int(suit) + 4);
    }

    if (x7cc_transitionSuit != suit) {
      x7cc_transitionSuit = suit;
      CAnimRes useRes = x7d0_animRes;
      useRes.SetCharacterNodeId(s32(x7cc_transitionSuit));
      SetModelData(std::make_unique<CModelData>(useRes));
      SetIntoBallReadyAnimation(mgr);
    }
    return;
  }
  case EPlayerMorphBallState::Unmorphing:
    if (x584_ballTransitionAnim == 14) // B_ball_unfurl
    {
      const float dur = x64_modelData->GetAnimationData()->GetAnimationDuration(x584_ballTransitionAnim);
      const float facRemaining = x64_modelData->GetAnimationData()->GetAnimTimeRemaining("Whole Body") / dur;
      if (facRemaining < 0.5f) {
        bool loop = false;
        x584_ballTransitionAnim = GetNextBallTransitionAnim(dt, loop, mgr);
        if (x64_modelData && x64_modelData->HasAnimData()) {
          const CAnimPlaybackParms parms(x584_ballTransitionAnim, -1, 1.f, true);
          x64_modelData->GetAnimationData()->SetAnimation(parms, false);
          x64_modelData->GetAnimationData()->EnableLooping(loop);
        }
      }
    } else if (x584_ballTransitionAnim != 5 && x584_ballTransitionAnim != 7)
    // ballstationarytoready_random, B_balljumptoairpose
    {
      const float velMag = zeus::CVector2f(x138_velocity.x(), x138_velocity.y()).magnitude();
      if (std::fabs(x58c_transitionVel - velMag) > 0.04f * GetActualFirstPersonMaxVelocity(dt) || velMag < 1.f) {
        bool loop = false;
        const s32 nextAnim = GetNextBallTransitionAnim(dt, loop, mgr);
        if (x64_modelData && x64_modelData->HasAnimData() && x584_ballTransitionAnim != nextAnim &&
            x584_ballTransitionAnim != 7) {
          x584_ballTransitionAnim = nextAnim;
          const CAnimPlaybackParms parms(x584_ballTransitionAnim, -1, 1.f, true);
          x64_modelData->GetAnimationData()->SetAnimation(parms, false);
          x64_modelData->GetAnimationData()->EnableLooping(loop);
          x58c_transitionVel = velMag;
        }
      }
    }
    break;
  default:
    break;
  }

  const SAdvancementDeltas deltas = UpdateAnimation(dt, mgr, true);
  MoveInOneFrameOR(deltas.x0_posDelta, dt);
  RotateInOneFrameOR(deltas.xc_rotDelta, dt);
  x574_morphTime = std::min(x574_morphTime + dt, x578_morphDuration);
  const float morphT = x574_morphTime / x578_morphDuration;
  if ((morphT >= 0.7f || x574_morphTime <= 2.f * dt) && x730_transitionModels.size() != 0) {
    x730_transitionModels.erase(x730_transitionModels.begin());
  }

  for (auto& m : x730_transitionModels) {
    m->AdvanceAnimation(dt, mgr, kInvalidAreaId, true);
  }

  const CGameCamera* cam = mgr.GetCameraManager()->GetCurrentCamera(mgr);
  x588_alpha = GetTransitionAlpha(cam->GetTranslation(), cam->GetNearClipDistance());

  if (x2f8_morphBallState == EPlayerMorphBallState::Morphing && morphT > 0.93f) {
    x588_alpha *= std::min(1.f, 1.f - (morphT - 0.93f) / 0.07f + 0.2f);
    xb4_drawFlags = CModelFlags(5, 0, 1, zeus::CColor(1.f, x588_alpha));
  } else if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphing && x588_alpha < 1.f) {
    if (x588_alpha > 0.05f) {
      xb4_drawFlags = CModelFlags(5, 0, 0x21, zeus::CColor(1.f, x588_alpha));
    } else {
      xb4_drawFlags = CModelFlags(5, 0, 1, zeus::CColor(1.f, x588_alpha));
    }
  } else {
    xb4_drawFlags = CModelFlags(5, 0, 3, zeus::CColor(1.f, x588_alpha));
  }

  x594_transisionBeamXfs.AddValue(x7f4_gunWorldXf);
  x658_transitionModelXfs.AddValue(x34_transform);
  x71c_transitionModelAlphas.AddValue(x588_alpha);

  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Unmorphing:
    (void)GetCollisionPrimitive()->CalculateAABox(GetPrimitiveTransform()).center();
    ClearForcesAndTorques();
    SetAngularVelocityWR(zeus::CAxisAngle());
    if (x574_morphTime >= x578_morphDuration || mgr.GetCameraManager()->IsInCinematicCamera()) {
      x824_transitionFilterTimer = std::max(x824_transitionFilterTimer, 0.95f);
      zeus::CVector3f pos;
      if (CanLeaveMorphBallState(mgr, pos)) {
        SetTranslation(GetTranslation() + pos);
        LeaveMorphBallState(mgr);
        xb4_drawFlags = CModelFlags(0, 0, 3, zeus::skWhite);
      } else {
        x574_morphTime = x578_morphDuration - x574_morphTime;
        TransitionToMorphBallState(dt, mgr);
      }
    }
    break;
  case EPlayerMorphBallState::Morphing:
    ClearForcesAndTorques();
    SetAngularVelocityWR(zeus::CAxisAngle());
    if (x574_morphTime >= x578_morphDuration || mgr.GetCameraManager()->IsInCinematicCamera()) {
      if (CanEnterMorphBallState(mgr, 1.f)) {
        ActivateMorphBallCamera(mgr);
        EnterMorphBallState(mgr);
        xb4_drawFlags = CModelFlags(0, 0, 3, zeus::skWhite);
      } else {
        x574_morphTime = x578_morphDuration - x574_morphTime;
        TransitionFromMorphBallState(mgr);
      }
    }
    if (x578_morphDuration != 0.f) {
      if (zeus::clamp(0.f, x574_morphTime, 1.f) >= 0.5f) {
        if (!x768_morphball->IsMorphBallTransitionFlashValid()) {
          x768_morphball->ResetMorphBallTransitionFlash();
        }
      }
    }
    break;
  default:
    break;
  }
}

void CPlayer::UpdateGunAlpha() {
  switch (x498_gunHolsterState) {
  case EGunHolsterState::Holstered:
    x494_gunAlpha = 0.f;
    break;
  case EGunHolsterState::Holstering:
    x494_gunAlpha = zeus::clamp(0.f, x49c_gunHolsterRemTime / g_tweakPlayerGun->GetGunHolsterTime(), 1.f);
    break;
  case EGunHolsterState::Drawing:
    x494_gunAlpha = 1.f - zeus::clamp(0.f, x49c_gunHolsterRemTime / 0.45f, 1.f);
    break;
  case EGunHolsterState::Drawn:
    x494_gunAlpha = 1.f;
    break;
  }
}

void CPlayer::UpdatePlayerSounds(float dt) {
  if (x784_damageSfxTimer <= 0.f) {
    return;
  }

  x784_damageSfxTimer -= dt;
  if (x784_damageSfxTimer > 0.f) {
    return;
  }

  CSfxManager::SfxStop(x770_damageLoopSfx);
  x770_damageLoopSfx.reset();
}

void CPlayer::Update(float dt, CStateManager& mgr) {
  SetCoefficientOfRestitutionModifier(0.f);
  UpdateMorphBallTransition(dt, mgr);

  const CPlayerState::EBeamId newBeam = mgr.GetPlayerState()->GetCurrentBeam();
  if (newBeam != x7ec_beam) {
    x7ec_beam = newBeam;
    x7f0_ballTransitionBeamModel = std::make_unique<CModelData>(
        CStaticRes(g_tweakPlayerRes->GetBeamBallTransitionModel(x7ec_beam), x7d8_beamScale));
  }

  if (!mgr.GetPlayerState()->IsPlayerAlive()) {
    if (x9f4_deathTime == 0.f) {
      CSfxManager::KillAll(CSfxManager::ESfxChannels::Game);
      CStreamAudioManager::StopAll();
      if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
        CSfxHandle hnd = CSfxManager::SfxStart(SFXsam_death, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
        ApplySubmergedPitchBend(hnd);
      }
    }

    const float prevDeathTime = x9f4_deathTime;
    x9f4_deathTime += dt;
    if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed) {
      if (x9f4_deathTime >= 1.f && prevDeathTime < 1.f) {
        xa00_deathPowerBomb = x490_gun->DropPowerBomb(mgr);
      }
      if (x9f4_deathTime >= 4.f && prevDeathTime < 4.f) {
        CSfxHandle hnd = CSfxManager::SfxStart(SFXsam_death, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
        ApplySubmergedPitchBend(hnd);
      }
    }
  }

  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Unmorphed:
  case EPlayerMorphBallState::Morphing:
  case EPlayerMorphBallState::Unmorphing:
    x7f4_gunWorldXf = x34_transform * x64_modelData->GetScaledLocatorTransform("GUN_LCTR");
    break;
  case EPlayerMorphBallState::Morphed:
    break;
  }

  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
    UpdateAimTargetTimer(dt);
    UpdateAimTarget(mgr);
    UpdateOrbitModeTimer(dt);
  }
  UpdateOrbitPreventionTimer(dt);
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    x768_morphball->Update(dt, mgr);
  } else {
    x768_morphball->StopSounds();
  }
  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphing ||
      x2f8_morphBallState == EPlayerMorphBallState::Morphing) {
    x768_morphball->UpdateEffects(dt, mgr);
  }
  UpdateGunAlpha();
  UpdateDebugCamera(mgr);
  UpdateVisorTransition(dt, mgr);
  mgr.SetActorAreaId(*this, mgr.GetWorld()->GetCurrentAreaId());
  UpdatePlayerSounds(dt);
  if (x26c_attachedActor != kInvalidUniqueId) {
    x270_attachedActorTime += dt;
  }

  x740_staticTimer = std::max(0.f, x740_staticTimer - dt);
  if (x740_staticTimer > 0.f) {
    x74c_visorStaticAlpha = std::max(0.f, x74c_visorStaticAlpha - x744_staticOutSpeed * dt);
  } else {
    x74c_visorStaticAlpha = std::min(1.f, x74c_visorStaticAlpha + x748_staticInSpeed * dt);
  }

  x274_energyDrain.ProcessEnergyDrain(mgr, dt);
  x4a4_moveSpeedAvg.AddValue(x4f8_moveSpeed);

  mgr.GetPlayerState()->UpdateStaticInterference(mgr, dt);
  if (!ShouldSampleFailsafe(mgr)) {
    CPhysicsActor::Stop();
  }

  if (IsEnergyLow(mgr)) {
    xa30_samusExhaustedVoiceTimer -= dt;
  } else {
    xa30_samusExhaustedVoiceTimer = 4.f;
  }

  if (!mgr.GetCameraManager()->IsInCinematicCamera() && xa30_samusExhaustedVoiceTimer <= 0.f) {
    StartSamusVoiceSfx(SFXsam_vox_exhausted, 1.f, 7);
    xa30_samusExhaustedVoiceTimer = 4.f;
  }
}

float CPlayer::UpdateCameraBob(float dt, CStateManager& mgr) {
  float bobMag = 0.f;
  CPlayerCameraBob::ECameraBobState state;
  const zeus::CVector3f backupVel = x138_velocity;
  if (x304_orbitState == EPlayerOrbitState::NoOrbit) {
    bobMag = std::fabs(backupVel.dot(x34_transform.basis[1]) / GetActualFirstPersonMaxVelocity(dt));
    state = CPlayerCameraBob::ECameraBobState::Walk;
    if (bobMag < 0.01f) {
      bobMag = 0.f;
      state = CPlayerCameraBob::ECameraBobState::WalkNoBob;
    }
  } else {
    state = CPlayerCameraBob::ECameraBobState::Orbit;
    const float f29 = backupVel.dot(x34_transform.basis[0]);
    const float f30 = backupVel.dot(x34_transform.basis[1]);
    const float maxVel = GetActualFirstPersonMaxVelocity(dt);
    const float strafeDist =
        skStrafeDistances[size_t(x2b0_outOfWaterTicks == 2 ? x2ac_surfaceRestraint : ESurfaceRestraints::Water)];
    bobMag = std::min(std::sqrt(f30 * f30 + f29 * f29) / std::sqrt(strafeDist * strafeDist + maxVel * maxVel) *
                          CPlayerCameraBob::GetOrbitBobScale(),
                      CPlayerCameraBob::GetMaxOrbitBobScale());
    if (bobMag < 0.01f) {
      bobMag = 0.f;
    }
  }

  if (x258_movementState != EPlayerMovementState::OnGround) {
    bobMag = 0.f;
    state = CPlayerCameraBob::ECameraBobState::InAir;
  } else if (bobMag < 0.01f) {
    if (x490_gun->GetLastFireButtonStates() != 0) {
      bobMag = 0.f;
      state = CPlayerCameraBob::ECameraBobState::GunFireNoBob;
    } else if (std::fabs(GetAngularVelocityOR().angle()) > 0.1f) {
      bobMag = 0.f;
      state = CPlayerCameraBob::ECameraBobState::TurningNoBob;
    }
  }

  if (x3dc_inFreeLook || x3dd_lookButtonHeld) {
    bobMag = 0.f;
    state = CPlayerCameraBob::ECameraBobState::FreeLookNoBob;
  }

  if (x304_orbitState == EPlayerOrbitState::Grapple) {
    bobMag = 0.f;
    state = CPlayerCameraBob::ECameraBobState::GrappleNoBob;
  }

  if (x3a8_scanState == EPlayerScanState::ScanComplete) {
    bobMag = 0.f;
  }

  if (x38c_doneSidewaysDashing) {
    bobMag *= 0.1f;
    state = CPlayerCameraBob::ECameraBobState::FreeLookNoBob;
    if (x258_movementState == EPlayerMovementState::OnGround) {
      x38c_doneSidewaysDashing = false;
    }
  }

  if (mgr.GetCameraManager()->IsInCinematicCamera()) {
    bobMag = 0.f;
  }

  bobMag *= mgr.GetCameraManager()->GetCameraBobMagnitude();

  x76c_cameraBob->SetPlayerVelocity(backupVel);
  x76c_cameraBob->SetState(state, mgr);
  x76c_cameraBob->SetBobMagnitude(bobMag);
  x76c_cameraBob->SetBobTimeScale((1.f - CPlayerCameraBob::GetSlowSpeedPeriodScale()) * bobMag +
                                  CPlayerCameraBob::GetSlowSpeedPeriodScale());
  x76c_cameraBob->Update(dt, mgr);

  return bobMag;
}

float CPlayer::GetAcceleration() const {
  if (x2d0_curAcceleration >= x2b4_accelerationTable.size()) {
    return x2b4_accelerationTable.back();
  }
  return x2b4_accelerationTable[x2d0_curAcceleration];
}

float CPlayer::CalculateOrbitMinDistance(EPlayerOrbitType type) const {
  return zeus::clamp(1.f, std::fabs(x314_orbitPoint.z() - GetTranslation().z()) / 20.f, 4.f) *
         g_tweakPlayer->GetOrbitMinDistance(int(type));
}

void CPlayer::PostUpdate(float dt, CStateManager& mgr) {
  UpdateArmAndGunTransforms(dt, mgr);

  float grappleSwingT;
  if (x3b8_grappleState != EGrappleState::Swinging) {
    grappleSwingT = 0.f;
  } else {
    grappleSwingT = x3bc_grappleSwingTimer / g_tweakPlayer->GetGrappleSwingPeriod();
  }

  float cameraBobT = 0.f;
  if (mgr.GetCameraManager()->IsInCinematicCamera()) {
    x76c_cameraBob = std::make_unique<CPlayerCameraBob>(CPlayerCameraBob::ECameraBobType::One,
                                                        CPlayerCameraBob::GetCameraBobExtent(),
                                                        CPlayerCameraBob::GetCameraBobPeriod());
  } else {
    cameraBobT = UpdateCameraBob(dt, mgr);
  }

  x490_gun->Update(grappleSwingT, cameraBobT, dt, mgr);
  UpdateOrbitTarget(mgr);
  UpdateOrbitOrientation(mgr);
}

bool CPlayer::StartSamusVoiceSfx(u16 sfx, float vol, int prio) {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    return false;
  }

  bool started = true;
  if (x77c_samusVoiceSfx) {
    if (CSfxManager::IsPlaying(x77c_samusVoiceSfx)) {
      started = false;
      if (prio > x780_samusVoicePriority) {
        CSfxManager::SfxStop(x77c_samusVoiceSfx);
        started = true;
      }
    }

    if (started) {
      x77c_samusVoiceSfx = CSfxManager::SfxStart(sfx, vol, 0.f, false, 0x7f, false, kInvalidAreaId);
      x780_samusVoicePriority = prio;
    }
  }

  return started;
}

bool CPlayer::IsPlayerDeadEnough() const {
  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
    return x9f4_deathTime > 2.5f;
  }

  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    return x9f4_deathTime > 6.f;
  }

  return false;
}

void CPlayer::AsyncLoadSuit(CStateManager& mgr) { x490_gun->AsyncLoadSuit(mgr); }

void CPlayer::LoadAnimationTokens() {
  TLockedToken<CDependencyGroup> transGroup = g_SimplePool->GetObj("BallTransition_DGRP");
  CDependencyGroup& group = *transGroup;
  x25c_ballTransitionsRes.reserve(group.GetObjectTagVector().size());
  for (const SObjectTag& tag : group.GetObjectTagVector()) {
    if (tag.type == FOURCC('CMDL') || tag.type == FOURCC('CSKR') || tag.type == FOURCC('TXTR')) {
      continue;
    }
    x25c_ballTransitionsRes.push_back(g_SimplePool->GetObj(tag));
  }
}

bool CPlayer::HasTransitionBeamModel() const {
  return x7f0_ballTransitionBeamModel && !x7f0_ballTransitionBeamModel->IsNull();
}

bool CPlayer::CanRenderUnsorted(const CStateManager& mgr) const { return false; }

const CDamageVulnerability* CPlayer::GetDamageVulnerability(const zeus::CVector3f& v1, const zeus::CVector3f& v2,
                                                            const CDamageInfo& info) const {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed && x570_immuneTimer > 0.f && !info.NoImmunity()) {
    return &CDamageVulnerability::ImmuneVulnerabilty();
  }
  return &CDamageVulnerability::NormalVulnerabilty();
}

const CDamageVulnerability* CPlayer::GetDamageVulnerability() const {
  constexpr CDamageInfo info(CWeaponMode(EWeaponType::Power, false, false, false), 0.f, 0.f, 0.f);
  return GetDamageVulnerability(zeus::skZero3f, zeus::skUp, info);
}

zeus::CVector3f CPlayer::GetHomingPosition(const CStateManager& mgr, float dt) const {
  if (dt > 0.f) {
    return x34_transform.origin + PredictMotion(dt).x0_translation;
  }
  return x34_transform.origin;
}

zeus::CVector3f CPlayer::GetAimPosition(const CStateManager& mgr, float dt) const {
  zeus::CVector3f ret = x34_transform.origin;
  if (dt > 0.f) {
    if (x304_orbitState == EPlayerOrbitState::NoOrbit) {
      ret += PredictMotion(dt).x0_translation;
    } else {
      ret = CSteeringBehaviors::ProjectOrbitalPosition(ret, x138_velocity, x314_orbitPoint, dt, xa04_preThinkDt);
    }
  }

  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    ret.z() += g_tweakPlayer->GetPlayerBallHalfExtent();
  } else {
    ret.z() += GetEyeHeight();
  }

  return ret;
}

void CPlayer::FluidFXThink(EFluidState state, CScriptWater& water, CStateManager& mgr) {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    x768_morphball->FluidFXThink(state, water, mgr);
    if (state == EFluidState::InFluid) {
      x9c5_30_selectFluidBallSound = true;
    }
  } else if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed) {
    if (mgr.GetFluidPlaneManager()->GetLastSplashDeltaTime(x8_uid) >= 0.2f) {
      zeus::CVector3f position(x34_transform.origin);
      position.z() = float(water.GetTriggerBoundsWR().max.z());
      mgr.GetFluidPlaneManager()->CreateSplash(x8_uid, mgr, water, position, 0.1f, state == EFluidState::EnteredFluid);
    }
  } else {
    if (mgr.GetFluidPlaneManager()->GetLastSplashDeltaTime(x8_uid) >= 0.2f) {
      zeus::CVector3f posOffset = x50c_moveDir;
      if (posOffset.canBeNormalized()) {
        posOffset = posOffset.normalized() * zeus::CVector3f(1.2f, 1.2f, 0.f);
      }
      switch (state) {
      case EFluidState::EnteredFluid: {
        bool doSplash = true;
        if (x4fc_flatMoveSpeed > 12.5f) {
          const zeus::CVector3f lookDir = x34_transform.basis[1].normalized();
          zeus::CVector3f dcVel = GetDampedClampedVelocityWR();
          dcVel.z() = 0.f;
          if (lookDir.dot(dcVel.normalized()) > 0.75f) {
            doSplash = false;
          }
        }
        if (doSplash) {
          zeus::CVector3f position = x34_transform.origin + posOffset;
          position.z() = float(water.GetTriggerBoundsWR().max.z());
          mgr.GetFluidPlaneManager()->CreateSplash(x8_uid, mgr, water, position, 0.3f, true);
          if (water.GetFluidPlane().GetFluidType() == EFluidType::NormalWater) {
            const float velMag = mgr.GetPlayer().GetVelocity().magnitude() / 10.f;
            mgr.GetEnvFxManager()->SetSplashRate(10.f * std::max(1.f, velMag));
          }
        }
        break;
      }
      case EFluidState::InFluid: {
        if (x138_velocity.magnitude() > 1.f && mgr.GetFluidPlaneManager()->GetLastRippleDeltaTime(x8_uid) >= 0.2f) {
          zeus::CVector3f position(x34_transform.origin);
          position.z() = float(water.GetTriggerBoundsWR().max.z());
          water.GetFluidPlane().AddRipple(0.5f, x8_uid, position, water, mgr);
        }
        break;
      }
      case EFluidState::LeftFluid: {
        zeus::CVector3f position = x34_transform.origin + posOffset;
        position.z() = float(water.GetTriggerBoundsWR().max.z());
        mgr.GetFluidPlaneManager()->CreateSplash(x8_uid, mgr, water, position, 0.15f, true);
        break;
      }
      default:
        break;
      }
    }
  }
}

void CPlayer::TakeDamage(bool significant, const zeus::CVector3f& location, float dam, EWeaponType type,
                         CStateManager& mgr) {
  if (!significant) {
    return;
  }

  if (dam >= 0.f) {
    x570_immuneTimer = 0.5f;
    x55c_damageAmt = dam;
    x560_prevDamageAmt = (type == EWeaponType::AI && dam == 0.00002f) ? 10.f : dam;
    x564_damageLocation = location;
    x558_wasDamaged = true;

    bool doRumble = false;
    u16 suitDamageSfx = 0;
    u16 damageLoopSfx = 0;
    u16 damageSamusVoiceSfx = 0;

    switch (type) {
    case EWeaponType::Phazon:
    case EWeaponType::OrangePhazon:
      damageLoopSfx = SFXphz_damage_lp;
      damageSamusVoiceSfx = SFXsam_vox_damage_phazon;
      break;
    case EWeaponType::PoisonWater:
      damageLoopSfx = SFXsam_damage_poison_lp;
      damageSamusVoiceSfx = SFXsam_vox_damage_poison;
      break;
    case EWeaponType::Lava:
      damageLoopSfx = SFXpds_lava_damage_lp;
      [[fallthrough]];
    case EWeaponType::Heat:
      damageSamusVoiceSfx = SFXsam_vox_damage_heat;
      break;
    default:
      if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
        if (dam > 30.f) {
          damageSamusVoiceSfx = SFXsam_vox_damage30;
        } else if (dam > 15.f) {
          damageSamusVoiceSfx = SFXsam_vox_damage15;
        } else {
          damageSamusVoiceSfx = SFXsam_vox_damage;
        }
        suitDamageSfx = SFXsam_suit_damage;
      } else {
        if (dam > 30.f) {
          suitDamageSfx = SFXsam_ball_damage30;
        } else if (dam > 15.f) {
          suitDamageSfx = SFXsam_ball_damage15;
        } else {
          suitDamageSfx = SFXsam_ball_damage;
        }
      }
      break;
    }

    if (damageSamusVoiceSfx != 0 && x774_samusVoiceTimeout <= 0.f) {
      StartSamusVoiceSfx(damageSamusVoiceSfx, 1.f, 8);
      x774_samusVoiceTimeout = mgr.GetActiveRandom()->Range(3.f, 4.f);
      doRumble = true;
    }

    if (damageLoopSfx != 0 && !x9c7_24_noDamageLoopSfx && xa2c_damageLoopSfxDelayTicks >= 2) {
      if (!x770_damageLoopSfx || x788_damageLoopSfxId != damageLoopSfx) {
        if (x770_damageLoopSfx && x788_damageLoopSfxId != damageLoopSfx) {
          CSfxManager::SfxStop(x770_damageLoopSfx);
        }
        x770_damageLoopSfx = CSfxManager::SfxStart(damageLoopSfx, 1.f, 0.f, false, 0x7f, true, kInvalidAreaId);
        x788_damageLoopSfxId = damageLoopSfx;
      }
      x784_damageSfxTimer = 0.5f;
    }

    if (suitDamageSfx != 0) {
      if (x770_damageLoopSfx) {
        CSfxManager::SfxStop(x770_damageLoopSfx);
        x770_damageLoopSfx.reset();
      }
      CSfxManager::SfxStart(suitDamageSfx, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
      x788_damageLoopSfxId = suitDamageSfx;
      xa2c_damageLoopSfxDelayTicks = 0;
      doRumble = true;
    }

    if (doRumble) {
      if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
        x490_gun->DamageRumble(location, dam, mgr);
      }

      float tmp = x55c_damageAmt / 25.f;
      if (std::fabs(tmp) > 1.f) {
        tmp = tmp > 0.f ? 1.f : -1.f;
      }

      mgr.GetRumbleManager().Rumble(mgr, ERumbleFxId::PlayerBump, tmp, ERumblePriority::One);
    }

    if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed) {
      x768_morphball->TakeDamage(x55c_damageAmt);
      x768_morphball->SetDamageTimer(0.4f);
    }
  }

  if (x3b8_grappleState != EGrappleState::None) {
    BreakGrapple(EPlayerOrbitRequest::DamageOnGrapple, mgr);
  }
}

void CPlayer::Accept(IVisitor& visitor) { visitor.Visit(this); }

CHealthInfo* CPlayer::HealthInfo(CStateManager& mgr) { return &mgr.GetPlayerState()->GetHealthInfo(); }

bool CPlayer::IsUnderBetaMetroidAttack(const CStateManager& mgr) const {
  if (x274_energyDrain.GetEnergyDrainIntensity() > 0.f) {
    for (const CEnergyDrainSource& source : x274_energyDrain.GetEnergyDrainSources()) {
      if (CPatterned::CastTo<MP1::CMetroidBeta>(mgr.GetObjectById(source.GetEnergyDrainSourceId()))) {
        return true;
      }
    }
  }

  return false;
}

std::optional<zeus::CAABox> CPlayer::GetTouchBounds() const {
  if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
    return GetBoundingBox();
  }

  const float ballTouchRad = x768_morphball->GetBallTouchRadius();
  const zeus::CVector3f ballCenter = GetTranslation() + zeus::CVector3f(0.f, 0.f, x768_morphball->GetBallRadius());
  return zeus::CAABox(ballCenter - ballTouchRad, ballCenter + ballTouchRad);
}

void CPlayer::DoPreThink(float dt, CStateManager& mgr) {
  PreThink(dt, mgr);
  if (CEntity* ent = mgr.ObjectById(xa00_deathPowerBomb)) {
    ent->PreThink(dt, mgr);
  }
}

void CPlayer::DoThink(float dt, CStateManager& mgr) {
  Think(dt, mgr);
  if (CEntity* ent = mgr.ObjectById(xa00_deathPowerBomb)) {
    ent->Think(dt, mgr);
  }
}

void CPlayer::UpdateScanningState(const CFinalInput& input, CStateManager& mgr, float dt) {
  if (mgr.GetPlayerState()->GetCurrentVisor() != CPlayerState::EPlayerVisor::Scan) {
    SetScanningState(EPlayerScanState::NotScanning, mgr);
    return;
  }

  if (x3a8_scanState != EPlayerScanState::NotScanning && x3b4_scanningObject != x310_orbitTargetId &&
      x310_orbitTargetId != kInvalidUniqueId) {
    SetScanningState(EPlayerScanState::NotScanning, mgr);
  }

  switch (x3a8_scanState) {
  case EPlayerScanState::NotScanning:
    if (ValidateScanning(input, mgr)) {
      if (const TCastToConstPtr<CActor> act = mgr.ObjectById(x310_orbitTargetId)) {
        const CScannableObjectInfo* scanInfo = act->GetScannableObjectInfo();
        float scanTime = mgr.GetPlayerState()->GetScanTime(scanInfo->GetScannableObjectId());
        if (scanTime >= 1.f) {
          x9c6_30_newScanScanning = false;
          scanTime = 1.f;
        } else {
          x9c6_30_newScanScanning = true;
        }

        SetScanningState(EPlayerScanState::Scanning, mgr);
        x3ac_scanningTime = scanTime * scanInfo->GetTotalDownloadTime();
        x3b0_curScanTime = 0.f;
      }
    }
    break;
  case EPlayerScanState::Scanning:
    if (ValidateScanning(input, mgr)) {
      if (const TCastToConstPtr<CActor> act = mgr.ObjectById(x310_orbitTargetId)) {
        if (const CScannableObjectInfo* scanInfo = act->GetScannableObjectInfo()) {
          x3ac_scanningTime = std::min(scanInfo->GetTotalDownloadTime(), x3ac_scanningTime + dt);
          x3b0_curScanTime += dt;
          mgr.GetPlayerState()->SetScanTime(scanInfo->GetScannableObjectId(),
                                            x3ac_scanningTime / scanInfo->GetTotalDownloadTime());
          if (x3ac_scanningTime >= scanInfo->GetTotalDownloadTime() &&
              x3b0_curScanTime >= g_tweakGui->GetScanSidesStartTime()) {
            SetScanningState(EPlayerScanState::ScanComplete, mgr);
          }
        }
      } else {
        SetScanningState(EPlayerScanState::NotScanning, mgr);
      }
    } else {
      SetScanningState(EPlayerScanState::NotScanning, mgr);
    }
    break;
  case EPlayerScanState::ScanComplete:
    if (!ValidateScanning(input, mgr)) {
      SetScanningState(EPlayerScanState::NotScanning, mgr);
    }
    break;
  }
}

bool CPlayer::ValidateScanning(const CFinalInput& input, const CStateManager& mgr) const {
  if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::ScanItem, input)) {
    if (x304_orbitState == EPlayerOrbitState::OrbitObject) {
      if (const TCastToConstPtr<CActor> act = mgr.ObjectById(x310_orbitTargetId)) {
        if (act->GetMaterialList().HasMaterial(EMaterialTypes::Scannable)) {
          const zeus::CVector3f targetToPlayer = GetTranslation() - act->GetTranslation();
          if (targetToPlayer.canBeNormalized() && targetToPlayer.magnitude() < g_tweakPlayer->GetScanningRange()) {
            return true;
          }
        }
      }
    }
  }

  return false;
}

static bool IsDataLoreResearchScan(CAssetId id) {
  const auto it = g_MemoryCardSys->LookupScanState(id);
  if (it == g_MemoryCardSys->GetScanStates().cend()) {
    return false;
  }

  switch (it->second) {
  case CSaveWorld::EScanCategory::Data:
  case CSaveWorld::EScanCategory::Lore:
  case CSaveWorld::EScanCategory::Research:
    return true;
  default:
    return false;
  }
}

static CAssetId UpdatePersistentScanPercent(u32 prevLogScans, u32 logScans, u32 totalLogScans) {
  if (prevLogScans == logScans) {
    return {};
  }

  const float scanPercent = logScans / float(totalLogScans) * 100.f;
  const float prevScanPercent = prevLogScans / float(totalLogScans) * 100.f;
  const float scanMessageInterval = g_tweakSlideShow->GetScanPercentInterval();
  const auto scanPercentProgStep = int(scanPercent / scanMessageInterval);
  const auto prevScanPercentProgStep = int(prevScanPercent / scanMessageInterval);
  const bool firstTime = scanPercent > g_GameState->SystemOptions().GetLogScanPercent();

  if (firstTime) {
    g_GameState->SystemOptions().SetLogScanPercent(u32(scanPercent));
  }

  if (scanPercentProgStep > prevScanPercentProgStep) {
    const char* const messageResBase = UnlockMessageResBases[zeus::clamp(0, scanPercentProgStep - 1, 1)];
    const auto message = std::string(messageResBase).append(1, firstTime ? '1' : '2');
    const auto* const id = g_ResFactory->GetResourceIdByName(message);
    if (id != nullptr) {
      return id->id;
    }
  }

  return {};
}

void CPlayer::FinishNewScan(CStateManager& mgr) {
  const TCastToConstPtr<CActor> act = mgr.ObjectById(x310_orbitTargetId);

  if (!act) {
    return;
  }

  if (!act->GetMaterialList().HasMaterial(EMaterialTypes::Scannable)) {
    return;
  }

  const auto* const scanInfo = act->GetScannableObjectInfo();
  if (!scanInfo) {
    return;
  }

  if (mgr.GetPlayerState()->GetScanTime(scanInfo->GetScannableObjectId()) < 1.f) {
    return;
  }

  if (!IsDataLoreResearchScan(scanInfo->GetScannableObjectId())) {
    return;
  }

  const auto scanCompletion = mgr.CalculateScanCompletionRate();
  const CAssetId message = UpdatePersistentScanPercent(mgr.GetPlayerState()->GetLogScans(), scanCompletion.first,
                                                 scanCompletion.second);
  if (message.IsValid()) {
    mgr.ShowPausedHUDMemo(message, 0.f);
  }
  mgr.GetPlayerState()->SetScanCompletionRate(scanCompletion);
}

void CPlayer::SetScanningState(EPlayerScanState state, CStateManager& mgr) {
  if (x3a8_scanState == state) {
    return;
  }

  mgr.SetGameState(CStateManager::EGameState::Running);
  if (x3a8_scanState == EPlayerScanState::ScanComplete) {
    if (TCastToPtr<CActor> act = mgr.ObjectById(x3b4_scanningObject)) {
      act->OnScanStateChanged(EScanState::Done, mgr);
    }
  }

  switch (state) {
  case EPlayerScanState::NotScanning:
    if (x3a8_scanState == EPlayerScanState::Scanning || x3a8_scanState == EPlayerScanState::ScanComplete) {
      if (x9c6_30_newScanScanning) {
        FinishNewScan(mgr);
      }
    }
    x3ac_scanningTime = 0.f;
    x3b0_curScanTime = 0.f;
    if (!g_tweakPlayer->GetScanRetention()) {
      if (const TCastToConstPtr<CActor> act = mgr.ObjectById(x310_orbitTargetId)) {
        if (act->GetMaterialList().HasMaterial(EMaterialTypes::Scannable)) {
          if (const auto* scanInfo = act->GetScannableObjectInfo()) {
            if (mgr.GetPlayerState()->GetScanTime(scanInfo->GetScannableObjectId()) < 1.f) {
              mgr.GetPlayerState()->SetScanTime(scanInfo->GetScannableObjectId(), 0.f);
            }
          }
        }
      }
    }
    x3b4_scanningObject = kInvalidUniqueId;
    break;
  case EPlayerScanState::Scanning:
    x3b4_scanningObject = x310_orbitTargetId;
    break;
  case EPlayerScanState::ScanComplete:
    if (g_tweakPlayer->GetScanFreezesGame()) {
      mgr.SetGameState(CStateManager::EGameState::SoftPaused);
    }
    x3b4_scanningObject = x310_orbitTargetId;
    break;
  }

  x3a8_scanState = state;
}

bool CPlayer::GetExplorationMode() const {
  switch (x498_gunHolsterState) {
  case EGunHolsterState::Holstering:
  case EGunHolsterState::Holstered:
    return true;
  default:
    return false;
  }
}

bool CPlayer::GetCombatMode() const {
  switch (x498_gunHolsterState) {
  case EGunHolsterState::Drawing:
  case EGunHolsterState::Drawn:
    return true;
  default:
    return false;
  }
}

void CPlayer::RenderGun(const CStateManager& mgr, const zeus::CVector3f& pos) const {
  if (mgr.GetCameraManager()->IsInCinematicCamera()) {
    return;
  }

  if (x490_gun->GetGrappleArm().GetActive() &&
      x490_gun->GetGrappleArm().GetAnimState() != CGrappleArm::EArmState::Done) {
    x490_gun->GetGrappleArm().RenderGrappleBeam(mgr, pos);
  }

  if (mgr.GetPlayerState()->GetActiveVisor(mgr) == CPlayerState::EPlayerVisor::Scan &&
      mgr.GetPlayerState()->GetVisorTransitionFactor() >= 1.f) {
    return;
  }

  if ((mgr.GetCameraManager()->IsInFirstPersonCamera() && x2f4_cameraState == EPlayerCameraState::FirstPerson) ||
      (x2f8_morphBallState == EPlayerMorphBallState::Morphing &&
       x498_gunHolsterState == EGunHolsterState::Holstering)) {
    CBooModel::SetReflectionCube(m_reflectionCube);
    CModelFlags flags(5, 0, 3, zeus::CColor(1.f, x494_gunAlpha));
    flags.m_extendedShader = EExtendedShader::LightingCubeReflection;
    x490_gun->Render(mgr, pos, flags);
  }
}

void CPlayer::Render(CStateManager& mgr) {
  bool doRender = x2f4_cameraState != EPlayerCameraState::Spawned;
  if (!doRender) {
    if (TCastToConstPtr<CCinematicCamera> cam = mgr.GetCameraManager()->GetCurrentCamera(mgr)) {
      doRender = (x2f8_morphBallState == EPlayerMorphBallState::Morphed && cam->GetFlags() & 0x40);
    }
  }

  if (x2f4_cameraState != EPlayerCameraState::FirstPerson && doRender) {
    SCOPED_GRAPHICS_DEBUG_GROUP("CPlayer::Render", zeus::skOrange);
    CBooModel::SetReflectionCube(m_reflectionCube);
    bool doTransitionRender = false;
    bool doBallRender = false;
    switch (x2f8_morphBallState) {
    case EPlayerMorphBallState::Unmorphed:
      x64_modelData->Touch(mgr, 0);
      CPhysicsActor::Render(mgr);
      if (HasTransitionBeamModel()) {
        x7f0_ballTransitionBeamModel->Touch(mgr, 0);
        CModelFlags flags(0, 0, 3, zeus::skWhite);
        flags.m_extendedShader = EExtendedShader::LightingCubeReflection;
        x7f0_ballTransitionBeamModel->Render(mgr, x7f4_gunWorldXf, x90_actorLights.get(), flags);
      }
      break;
    case EPlayerMorphBallState::Morphing:
      x768_morphball->TouchModel(mgr);
      doTransitionRender = true;
      doBallRender = true;
      break;
    case EPlayerMorphBallState::Unmorphing:
      x490_gun->TouchModel(mgr);
      doTransitionRender = true;
      doBallRender = true;
      break;
    case EPlayerMorphBallState::Morphed:
      x64_modelData->Touch(mgr, 0);
      x768_morphball->Render(mgr, x90_actorLights.get());
      break;
    }

    if (doTransitionRender) {
      CPhysicsActor::Render(mgr);
      if (HasTransitionBeamModel()) {
        CModelFlags flags(5, 0, 3, zeus::CColor(1.f, x588_alpha));
        flags.m_extendedShader = EExtendedShader::LightingCubeReflection;
        x7f0_ballTransitionBeamModel->Render(CModelData::EWhichModel::Normal, x7f4_gunWorldXf, x90_actorLights.get(),
                                             flags);
      }

      float morphFactor = x574_morphTime / x578_morphDuration;
      float transitionAlpha;
      if (morphFactor < 0.05f) {
        transitionAlpha = 0.f;
      } else if (morphFactor < 0.1f) {
        transitionAlpha = (morphFactor - 0.05f) / 0.05f;
      } else if (morphFactor < 0.8f) {
        transitionAlpha = 1.f;
      } else {
        transitionAlpha = 1.f - (morphFactor - 0.8f) / 0.2f;
      }

      const auto mdsp1 = int(x730_transitionModels.size() + 1);
      for (int i = 0; i < x730_transitionModels.size(); ++i) {
        const int ni = i + 1;
        const float alpha = transitionAlpha * (1.f - (ni + 1) / float(mdsp1)) * *x71c_transitionModelAlphas.GetEntry(ni);
        if (alpha != 0.f) {
          CModelData& data = *x730_transitionModels[i];
          CModelFlags flags(5, 0, 3, zeus::CColor(1.f, alpha));
          flags.m_extendedShader = EExtendedShader::LightingCubeReflection;
          data.Render(CModelData::GetRenderingModel(mgr), *x658_transitionModelXfs.GetEntry(ni), x90_actorLights.get(),
                      flags);
          if (HasTransitionBeamModel()) {
            CModelFlags transFlags(5, 0, 3, zeus::CColor(1.f, alpha));
            transFlags.m_extendedShader = EExtendedShader::LightingCubeReflection;
            x7f0_ballTransitionBeamModel->Render(CModelData::EWhichModel::Normal, *x594_transisionBeamXfs.GetEntry(ni),
                                                 x90_actorLights.get(), transFlags);
          }
        }
      }

      if (doBallRender) {
        float morphFactor = x574_morphTime / x578_morphDuration;
        float ballAlphaStart = 0.75f;
        float ballAlphaMag = 4.f;
        if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphing) {
          ballAlphaStart = 0.875f;
          morphFactor = 1.f - morphFactor;
          ballAlphaMag = 8.f;
        }

        if (morphFactor > ballAlphaStart) {
          CModelFlags flags(5, u8(x768_morphball->GetMorphballModelShader()), 3,
                            zeus::CColor(1.f, ballAlphaMag * (morphFactor - ballAlphaStart)));
          flags.m_extendedShader = EExtendedShader::LightingCubeReflection;
          x768_morphball->GetMorphballModelData().Render(mgr, x768_morphball->GetBallToWorld(), x90_actorLights.get(),
                                                         flags);
        }

        if (x2f8_morphBallState == EPlayerMorphBallState::Morphing) {
          if (morphFactor > 0.5f) {
            float tmp = (morphFactor - 0.5f) / 0.5f;
            float rotate = 1.f - tmp;
            float scale = 0.75f * rotate + 1.f;
            float ballAlpha;
            if (tmp < 0.1f) {
              ballAlpha = 0.f;
            } else if (tmp < 0.2f) {
              ballAlpha = (tmp - 0.1f) / 0.1f;
            } else if (tmp < 0.9f) {
              ballAlpha = 1.f;
            } else {
              ballAlpha = 1.f - (morphFactor - 0.9f) / 0.1f;
            }

            const float theta = zeus::degToRad(360.f * rotate);
            ballAlpha *= 0.5f;
            if (ballAlpha > 0.f) {
              CModelFlags flags(7, 0, 3, zeus::CColor(1.f, ballAlpha));
              flags.m_extendedShader = EExtendedShader::LightingCubeReflection;
              x768_morphball->GetMorphballModelData().Render(
                  mgr,
                  x768_morphball->GetBallToWorld() * zeus::CTransform::RotateZ(theta) * zeus::CTransform::Scale(scale),
                  x90_actorLights.get(), flags);
            }
          }
          x768_morphball->RenderMorphBallTransitionFlash(mgr);
        }
      }
    }
  }
}

void CPlayer::RenderReflectedPlayer(CStateManager& mgr) {
  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Unmorphed:
  case EPlayerMorphBallState::Morphing:
  case EPlayerMorphBallState::Unmorphing:
    SetCalculateLighting(true);
    if (x2f4_cameraState == EPlayerCameraState::FirstPerson) {
      const zeus::CFrustum frustum;
      CActor::PreRender(mgr, frustum);
    }
    CPhysicsActor::Render(mgr);
    if (HasTransitionBeamModel()) {
      constexpr CModelFlags flags(0, 0, 3, zeus::skWhite);
      x7f0_ballTransitionBeamModel->Render(mgr, x7f4_gunWorldXf, nullptr, flags);
    }
    break;
  case EPlayerMorphBallState::Morphed:
    x768_morphball->Render(mgr, x90_actorLights.get());
    break;
  }
}

void CPlayer::PreRender(CStateManager& mgr, const zeus::CFrustum& frustum) {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    SetCalculateLighting(false);
    x768_morphball->PreRender(mgr, frustum);
  } else {
    SetCalculateLighting(true);
    if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
      x490_gun->PreRender(mgr, frustum, mgr.GetCameraManager()->GetGlobalCameraTranslation(mgr));
    }
  }

  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed || mgr.GetCameraManager()->IsInCinematicCamera()) {
    x768_morphball->DeleteBallShadow();
  } else {
    x768_morphball->CreateBallShadow();
    x768_morphball->RenderToShadowTex(mgr);
  }

  for (auto& model : x730_transitionModels) {
    model->GetAnimationData()->PreRender();
  }

  if (x2f4_cameraState != EPlayerCameraState::FirstPerson) {
    CActor::PreRender(mgr, frustum);
  }
}

void CPlayer::CalculateRenderBounds() {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    const float rad = x768_morphball->GetBallRadius();
    x9c_renderBounds = zeus::CAABox(GetTranslation() - zeus::CVector3f(rad, rad, 0.f),
                                    GetTranslation() + zeus::CVector3f(rad, rad, rad * 2.f));
  } else {
    CActor::CalculateRenderBounds();
  }
}

void CPlayer::AddToRenderer(const zeus::CFrustum& frustum, CStateManager& mgr) {
  if (x2f4_cameraState != EPlayerCameraState::FirstPerson && x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    if (x768_morphball->IsInFrustum(frustum)) {
      CActor::AddToRenderer(frustum, mgr);
    } else {
      x768_morphball->TouchModel(mgr);
    }
  } else {
    x490_gun->AddToRenderer(frustum, mgr);
    CActor::AddToRenderer(frustum, mgr);
  }
}

void CPlayer::ComputeFreeLook(const CFinalInput& input) {
  const float lookLeft = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookLeft, input);
  const float lookRight = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookRight, input);
  float lookUp = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookUp, input);
  float lookDown = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookDown, input);

  if (g_GameState->GameOptions().GetInvertYAxis()) {
    lookUp = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookDown, input);
    lookDown = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookUp, input);
  }

  if (!g_tweakPlayer->GetStayInFreeLookWhileFiring() &&
      (ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input) ||
       x304_orbitState != EPlayerOrbitState::NoOrbit)) {
    x3e8_horizFreeLookAngleVel = 0.f;
    x3f0_vertFreeLookAngleVel = 0.f;
  } else {
    if (x3dc_inFreeLook) {
      x3e8_horizFreeLookAngleVel = (lookLeft - lookRight) * g_tweakPlayer->GetHorizontalFreeLookAngleVel();
      x3f0_vertFreeLookAngleVel = (lookUp - lookDown) * g_tweakPlayer->GetVerticalFreeLookAngleVel();
    }
    if (!x3de_lookAnalogHeld || !x3dd_lookButtonHeld) {
      x3e8_horizFreeLookAngleVel = 0.f;
      x3f0_vertFreeLookAngleVel = 0.f;
    }
  }

  if (g_tweakPlayer->GetHoldButtonsForFreeLook()) {
    if ((g_tweakPlayer->GetTwoButtonsForFreeLook() &&
         (!ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold1, input) ||
          !ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold2, input))) ||
        (!ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold1, input) &&
         !ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold2, input))) {
      x3e8_horizFreeLookAngleVel = 0.f;
      x3f0_vertFreeLookAngleVel = 0.f;
    }
  }

  if (IsMorphBallTransitioning()) {
    x3e8_horizFreeLookAngleVel = 0.f;
    x3f0_vertFreeLookAngleVel = 0.f;
  }
}

void CPlayer::UpdateFreeLookState(const CFinalInput& input, float dt, CStateManager& mgr) {
  if (x304_orbitState == EPlayerOrbitState::ForcedOrbitObject || IsMorphBallTransitioning() ||
      x2f8_morphBallState != EPlayerMorphBallState::Unmorphed ||
      (x3b8_grappleState != EGrappleState::None && x3b8_grappleState != EGrappleState::Firing)) {
    x3dc_inFreeLook = false;
    x3dd_lookButtonHeld = false;
    x3de_lookAnalogHeld = false;
    x3e8_horizFreeLookAngleVel = 0.f;
    x3f0_vertFreeLookAngleVel = 0.f;
    x9c4_25_showCrosshairs = false;
    return;
  }

  if (g_tweakPlayer->GetHoldButtonsForFreeLook()) {
    if ((g_tweakPlayer->GetTwoButtonsForFreeLook() &&
         (ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold1, input) &&
          ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold2, input))) ||
        (!g_tweakPlayer->GetTwoButtonsForFreeLook() &&
         (ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold1, input) ||
          ControlMapper::GetDigitalInput(ControlMapper::ECommands::LookHold2, input)))) {
      if (!x3dd_lookButtonHeld) {
        const zeus::CVector3f lookDir = mgr.GetCameraManager()->GetFirstPersonCamera()->GetTransform().basis[1];
        zeus::CVector3f lookDirFlat = lookDir;
        lookDirFlat.z() = 0.f;
        x3e4_freeLookYawAngle = 0.f;
        if (lookDirFlat.canBeNormalized()) {
          lookDirFlat.normalize();
          x3ec_freeLookPitchAngle = std::acos(zeus::clamp(-1.f, lookDirFlat.dot(lookDir), 1.f));
          if (lookDir.z() < 0.f)
            x3ec_freeLookPitchAngle = -x3ec_freeLookPitchAngle;
        }
      }
      x3dc_inFreeLook = true;
      x3dd_lookButtonHeld = true;

      x3de_lookAnalogHeld = (ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookLeft, input) >= 0.1f ||
                             ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookRight, input) >= 0.1f ||
                             ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookDown, input) >= 0.1f ||
                             ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookUp, input) >= 0.1f);
    } else {
      x3dc_inFreeLook = false;
      x3dd_lookButtonHeld = false;
      x3de_lookAnalogHeld = false;
      x3e8_horizFreeLookAngleVel = 0.f;
      x3f0_vertFreeLookAngleVel = 0.f;
    }
  } else {
    x3de_lookAnalogHeld = (ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookLeft, input) >= 0.1f ||
                           ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookRight, input) >= 0.1f ||
                           ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookDown, input) >= 0.1f ||
                           ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookUp, input) >= 0.1f);
    x3dd_lookButtonHeld = false;
    if (std::fabs(x3e4_freeLookYawAngle) < g_tweakPlayer->GetFreeLookCenteredThresholdAngle() &&
        std::fabs(x3ec_freeLookPitchAngle) < g_tweakPlayer->GetFreeLookCenteredThresholdAngle()) {
      if (x3e0_curFreeLookCenteredTime > g_tweakPlayer->GetFreeLookCenteredTime()) {
        x3dc_inFreeLook = false;
        x3e8_horizFreeLookAngleVel = 0.f;
        x3f0_vertFreeLookAngleVel = 0.f;
      } else {
        x3e0_curFreeLookCenteredTime += dt;
      }
    } else {
      x3dc_inFreeLook = true;
      x3e0_curFreeLookCenteredTime = 0.f;
    }
  }

  UpdateCrosshairsState(input);
}

void CPlayer::UpdateFreeLook(float dt) {
  if (GetFrozenState()) {
    return;
  }

  float lookDeltaAngle = dt * g_tweakPlayer->GetFreeLookSpeed();
  if (!x3de_lookAnalogHeld) {
    lookDeltaAngle = dt * g_tweakPlayer->GetFreeLookSnapSpeed();
  }

  float angleVelP = x3f0_vertFreeLookAngleVel - x3ec_freeLookPitchAngle;
  const float vertLookDamp = zeus::clamp(0.f, std::fabs(angleVelP / 1.0471976f), 1.f);
  float dx = lookDeltaAngle * (2.f * vertLookDamp - std::sin((M_PIF / 2.f) * vertLookDamp));
  if (0.f <= angleVelP) {
    x3ec_freeLookPitchAngle += dx;
  } else {
    x3ec_freeLookPitchAngle -= dx;
  }

  angleVelP = x3e8_horizFreeLookAngleVel - x3e4_freeLookYawAngle;
  dx = lookDeltaAngle * zeus::clamp(0.f, std::fabs(angleVelP / g_tweakPlayer->GetHorizontalFreeLookAngleVel()), 1.f);
  if (0.f <= angleVelP) {
    x3e4_freeLookYawAngle += dx;
  } else {
    x3e4_freeLookYawAngle -= dx;
  }

  if (g_tweakPlayer->GetFreeLookTurnsPlayer()) {
    x3e4_freeLookYawAngle = 0.f;
  }
}

float CPlayer::GetMaximumPlayerPositiveVerticalVelocity(CStateManager& mgr) const {
  return mgr.GetPlayerState()->GetItemAmount(CPlayerState::EItemType::SpaceJumpBoots) ? 14.f : 11.666666f;
}

void CPlayer::StartLandingControlFreeze() {
  x760_controlsFrozen = true;
  x764_controlsFrozenTimeout = 0.75f;
}

void CPlayer::EndLandingControlFreeze() {
  x760_controlsFrozen = false;
  x764_controlsFrozenTimeout = 0.f;
}

void CPlayer::ProcessFrozenInput(float dt, CStateManager& mgr) {
  x764_controlsFrozenTimeout -= dt;
  if (x764_controlsFrozenTimeout <= 0.f) {
    EndLandingControlFreeze();
  } else {
    const CFinalInput dummy;
    if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
      x768_morphball->ComputeBallMovement(dummy, mgr, dt);
      x768_morphball->UpdateBallDynamics(mgr, dt);
    } else {
      ComputeMovement(dummy, mgr, dt);
    }
  }
}

void CPlayer::ProcessInput(const CFinalInput& input, CStateManager& mgr) {
  if (input.ControllerIdx() != 0) {
    return;
  }

  if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
    UpdateScanningState(input, mgr, input.DeltaTime());
  }

  if (mgr.GetGameState() != CStateManager::EGameState::Running) {
    return;
  }

  if (!mgr.GetPlayerState()->IsPlayerAlive()) {
    return;
  }

  if (GetFrozenState()) {
    UpdateFrozenState(input, mgr);
  }

  if (GetFrozenState()) {
    if (x258_movementState == EPlayerMovementState::OnGround ||
        x258_movementState == EPlayerMovementState::FallingMorphed) {
      return;
    }

    const CFinalInput dummyInput;
    if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
      x768_morphball->ComputeBallMovement(dummyInput, mgr, input.DeltaTime());
      x768_morphball->UpdateBallDynamics(mgr, input.DeltaTime());
    } else {
      ComputeMovement(dummyInput, mgr, input.DeltaTime());
    }

    return;
  }

  if (x760_controlsFrozen) {
    ProcessFrozenInput(input.DeltaTime(), mgr);
    return;
  }

  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed && x4a0_failsafeTest->Passes()) {
    const auto* prim = static_cast<const CCollidableAABox*>(GetCollisionPrimitive());
    const zeus::CAABox tmpAABB(prim->GetBox().min - 0.2f, prim->GetBox().max + 0.2f);
    const CCollidableAABox tmpBox(tmpAABB, prim->GetMaterial());
    CPhysicsActor::Stop();
    const zeus::CAABox testBounds = prim->GetBox().getTransformedAABox(x34_transform);
    const zeus::CAABox expandedBounds(testBounds.min - 3.f, testBounds.max + 3.f);
    CAreaCollisionCache cache(expandedBounds);
    CGameCollision::BuildAreaCollisionCache(mgr, cache);
    rstl::reserved_vector<TUniqueId, 1024> nearList;
    mgr.BuildColliderList(nearList, *this, expandedBounds);
    const std::optional<zeus::CVector3f> nonIntVec =
        CGameCollision::FindNonIntersectingVector(mgr, cache, *this, tmpBox, nearList);
    if (nonIntVec) {
      x4a0_failsafeTest->Reset();
      SetTranslation(GetTranslation() + *nonIntVec);
    }
  }

  UpdateGrappleState(input, mgr);
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    float leftDiv = g_tweakBall->GetLeftStickDivisor();
    const float rightDiv = g_tweakBall->GetRightStickDivisor();
    if (x26c_attachedActor != kInvalidUniqueId || IsUnderBetaMetroidAttack(mgr)) {
      leftDiv = 2.f;
    }
    const CFinalInput scaledInput = input.ScaleAnalogueSticks(leftDiv, rightDiv);
    x768_morphball->ComputeBallMovement(scaledInput, mgr, input.DeltaTime());
    x768_morphball->UpdateBallDynamics(mgr, input.DeltaTime());
    x4a0_failsafeTest->Reset();
  } else {
    if (x304_orbitState == EPlayerOrbitState::Grapple) {
      ApplyGrappleForces(input, mgr, input.DeltaTime());
    } else {
      const CFinalInput scaledInput = input.ScaleAnalogueSticks(IsUnderBetaMetroidAttack(mgr) ? 3.f : 1.f, 1.f);
      ComputeMovement(scaledInput, mgr, input.DeltaTime());
    }

    if (ShouldSampleFailsafe(mgr)) {
      CFailsafeTest::EInputState inputState = CFailsafeTest::EInputState::Moving;
      if (x258_movementState == EPlayerMovementState::ApplyJump) {
        inputState = CFailsafeTest::EInputState::StartingJump;
      } else if (x258_movementState == EPlayerMovementState::Jump) {
        inputState = CFailsafeTest::EInputState::Jump;
      }
      x4a0_failsafeTest->AddSample(inputState, GetTranslation(), x138_velocity,
                                   zeus::CVector2f(input.ALeftX(), input.ALeftY()));
    }
  }

  ComputeFreeLook(input);
  UpdateFreeLookState(input, input.DeltaTime(), mgr);
  UpdateOrbitInput(input, mgr);
  UpdateOrbitZone(mgr);
  UpdateGunState(input, mgr);
  UpdateVisorState(input, input.DeltaTime(), mgr);

  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed ||
      (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed && x498_gunHolsterState == EGunHolsterState::Drawn)) {
    x490_gun->ProcessInput(input, mgr);
    if (x2f8_morphBallState == EPlayerMorphBallState::Morphed && x26c_attachedActor != kInvalidUniqueId) {
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::TurnLeft, input) ||
          ControlMapper::GetPressInput(ControlMapper::ECommands::TurnRight, input) ||
          ControlMapper::GetPressInput(ControlMapper::ECommands::Forward, input) ||
          ControlMapper::GetPressInput(ControlMapper::ECommands::Backward, input) ||
          ControlMapper::GetPressInput(ControlMapper::ECommands::JumpOrBoost, input)) {
        xa28_attachedActorStruggle += input.DeltaTime() * 600.f * input.DeltaTime();
        if (xa28_attachedActorStruggle > 1.f) {
          xa28_attachedActorStruggle = 1.f;
        }
      } else {
        const float tmp = 7.5f * input.DeltaTime();
        xa28_attachedActorStruggle -= input.DeltaTime() * std::min(1.f, xa28_attachedActorStruggle * tmp + tmp);
        if (xa28_attachedActorStruggle < 0.f) {
          xa28_attachedActorStruggle = 0.f;
        }
      }
    }
  }

  UpdateCameraState(mgr);
  UpdateMorphBallState(input.DeltaTime(), input, mgr);
  UpdateCameraTimers(input.DeltaTime(), input);
  UpdateFootstepSounds(input, mgr, input.DeltaTime());
  x2a8_timeSinceJump += input.DeltaTime();

  if (CheckSubmerged()) {
    SetSoundEventPitchBend(0);
  } else {
    SetSoundEventPitchBend(8192);
  }

  CalculateLeaveMorphBallDirection(input);
}

bool CPlayer::ShouldSampleFailsafe(CStateManager& mgr) const {
  const TCastToConstPtr<CCinematicCamera> cineCam = mgr.GetCameraManager()->GetCurrentCamera(mgr);
  if (!mgr.GetPlayerState()->IsPlayerAlive()) {
    return false;
  }
  return x2f4_cameraState != EPlayerCameraState::Spawned || !cineCam || (cineCam->GetFlags() & 0x80) == 0;
}

void CPlayer::CalculateLeaveMorphBallDirection(const CFinalInput& input) {
  if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
    x518_leaveMorphDir = x50c_moveDir;
  } else {
    if (ControlMapper::GetAnalogInput(ControlMapper::ECommands::Forward, input) > 0.3f ||
        ControlMapper::GetAnalogInput(ControlMapper::ECommands::Backward, input) > 0.3f ||
        ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnLeft, input) > 0.3f ||
        ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnRight, input) > 0.3f) {
      if (x138_velocity.magnitude() > 0.5f) {
        x518_leaveMorphDir = x50c_moveDir;
      }
    }
  }
}

void CPlayer::CalculatePlayerControlDirection(CStateManager& mgr) {
  if (x9c4_30_controlDirOverride) {
    if (x9d8_controlDirOverrideDir.canBeNormalized()) {
      x540_controlDir = x9d8_controlDirOverrideDir.normalized();
      x54c_controlDirFlat = x9d8_controlDirOverrideDir;
      x54c_controlDirFlat.z() = 0.f;
      if (x54c_controlDirFlat.canBeNormalized()) {
        x54c_controlDirFlat.normalize();
      } else {
        x540_controlDir = x54c_controlDirFlat = zeus::skForward;
      }
    } else {
      x540_controlDir = x54c_controlDirFlat = zeus::skForward;
    }
  } else {
    const zeus::CVector3f camToPlayer = GetTranslation() - mgr.GetCameraManager()->GetCurrentCamera(mgr)->GetTranslation();
    if (!camToPlayer.canBeNormalized()) {
      x540_controlDir = x54c_controlDirFlat = zeus::skForward;
    } else {
      zeus::CVector3f camToPlayerFlat(camToPlayer.x(), camToPlayer.y(), 0.f);
      if (camToPlayerFlat.canBeNormalized()) {
        if (camToPlayerFlat.magnitude() > g_tweakBall->GetBallCameraControlDistance()) {
          x540_controlDir = camToPlayer.normalized();
          if (camToPlayerFlat.canBeNormalized()) {
            camToPlayerFlat.normalize();
            switch (x2f8_morphBallState) {
            case EPlayerMorphBallState::Morphed:
              x54c_controlDirFlat = camToPlayerFlat;
              break;
            default:
              x540_controlDir = GetTransform().basis[1];
              x54c_controlDirFlat = x540_controlDir;
              x54c_controlDirFlat.z() = 0.f;
              if (x54c_controlDirFlat.canBeNormalized()) {
                x54c_controlDirFlat.normalize();
              }
            }
          } else if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
            x540_controlDir = GetTransform().basis[1];
            x54c_controlDirFlat.z() = 0.f;
            if (x54c_controlDirFlat.canBeNormalized()) {
              x54c_controlDirFlat.normalize();
            }
          }
        } else {
          if (x4fc_flatMoveSpeed < 0.25f) {
            x540_controlDir = camToPlayer;
            x54c_controlDirFlat = camToPlayerFlat;
          } else if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
            x540_controlDir = GetTransform().basis[1];
            x54c_controlDirFlat.z() = 0.f;
            if (x54c_controlDirFlat.canBeNormalized()) {
              x54c_controlDirFlat.normalize();
            }
          }
        }
      }
    }
  }
}

void CPlayer::CalculatePlayerMovementDirection(float dt) {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphing ||
      x2f8_morphBallState == EPlayerMorphBallState::Unmorphing) {
    return;
  }

  const zeus::CVector3f delta = GetTranslation() - x524_lastPosForDirCalc;
  if (delta.canBeNormalized() && delta.magnitude() > 0.02f) {
    x53c_timeMoving += dt;
    x4f8_moveSpeed = std::fabs(delta.magnitude() / dt);
    x500_lookDir = delta.normalized();
    zeus::CVector3f flatDelta(delta.x(), delta.y(), 0.f);
    if (flatDelta.canBeNormalized()) {
      x4fc_flatMoveSpeed = std::fabs(flatDelta.magnitude() / dt);
      flatDelta.normalize();
      switch (x2f8_morphBallState) {
      case EPlayerMorphBallState::Morphed:
        if (x4fc_flatMoveSpeed > 0.25f) {
          x50c_moveDir = flatDelta;
        }
        x530_gunDir = x50c_moveDir;
        x524_lastPosForDirCalc = GetTranslation();
        break;
      default:
        x500_lookDir = GetTransform().basis[1];
        x50c_moveDir = x500_lookDir;
        x50c_moveDir.z() = 0.f;
        if (x50c_moveDir.canBeNormalized()) {
          x50c_moveDir.normalize();
        }
        x530_gunDir = x50c_moveDir;
        x524_lastPosForDirCalc = GetTranslation();
        break;
      }
    } else {
      if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
        x500_lookDir = GetTransform().basis[1];
        x50c_moveDir = x500_lookDir;
        x50c_moveDir.z() = 0.f;
        if (x50c_moveDir.canBeNormalized()) {
          x50c_moveDir.normalize();
        }
        x530_gunDir = x50c_moveDir;
        x524_lastPosForDirCalc = GetTranslation();
      }
      x4fc_flatMoveSpeed = 0.f;
    }
  } else {
    x53c_timeMoving = 0.f;
    switch (x2f8_morphBallState) {
    case EPlayerMorphBallState::Morphed:
    case EPlayerMorphBallState::Morphing:
    case EPlayerMorphBallState::Unmorphing:
      x500_lookDir = x50c_moveDir;
      break;
    default:
      x500_lookDir = GetTransform().basis[1];
      x50c_moveDir = x500_lookDir;
      x50c_moveDir.z() = 0.f;
      if (x50c_moveDir.canBeNormalized()) {
        x50c_moveDir.normalize();
      }
      x530_gunDir = x50c_moveDir;
      x524_lastPosForDirCalc = GetTranslation();
      break;
    }
    x4f8_moveSpeed = 0.f;
    x4fc_flatMoveSpeed = 0.f;
  }

  x50c_moveDir.z() = 0.f;
  if (x50c_moveDir.canBeNormalized()) {
    x500_lookDir.normalize();
  }
}

void CPlayer::UnFreeze(CStateManager& stateMgr) {
  if (!GetFrozenState()) {
    return;
  }

  x750_frozenTimeout = 0.f;
  x754_iceBreakJumps = 0;
  CPhysicsActor::Stop();
  ClearForcesAndTorques();
  RemoveMaterial(EMaterialTypes::Immovable, stateMgr);
  if (!stateMgr.GetCameraManager()->IsInCinematicCamera() && xa0c_iceTextureId.IsValid()) {
    std::optional<TToken<CGenDescription>> gpsm;
    gpsm.emplace(g_SimplePool->GetObj(SObjectTag(FOURCC('PART'), xa0c_iceTextureId)));
    auto* effect = new CHUDBillboardEffect(gpsm, {}, stateMgr.AllocateUniqueId(), true, "FrostExplosion",
                                           CHUDBillboardEffect::GetNearClipDistance(stateMgr),
                                           CHUDBillboardEffect::GetScaleForPOV(stateMgr), zeus::skWhite, zeus::skOne3f,
                                           zeus::skZero3f);
    stateMgr.AddObject(effect);
    CSfxHandle hnd = CSfxManager::SfxStart(SFXcrk_break_final, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
    ApplySubmergedPitchBend(hnd);
  }

  x768_morphball->Stop();
  SetVisorSteam(0.f, 6.f / 14.f, 1.f / 14.f, xa08_steamTextureId, false);
}

void CPlayer::Freeze(CStateManager& stateMgr, CAssetId steamTxtr, u16 sfx, CAssetId iceTxtr) {
  if (stateMgr.GetCameraManager()->IsInCinematicCamera() || GetFrozenState()) {
    return;
  }

  bool showMsg;
  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
    showMsg = g_GameState->SystemOptions().GetShowFrozenFpsMessage();
  } else {
    showMsg = g_GameState->SystemOptions().GetShowFrozenBallMessage();
  }

  if (showMsg) {
    const char16_t* msg = g_MainStringTable->GetString(int(x2f8_morphBallState >= EPlayerMorphBallState::Morphed) + 19);
    const CHUDMemoParms parms(5.f, true, false, false);
    MP1::CSamusHud::DisplayHudMemo(msg, parms);
  }

  x750_frozenTimeout = x758_frozenTimeoutBias + g_tweakPlayer->GetFrozenTimeout();
  x754_iceBreakJumps = -x75c_additionalIceBreakJumps;

  CPhysicsActor::Stop();
  ClearForcesAndTorques();
  if (x3b8_grappleState != EGrappleState::None) {
    BreakGrapple(EPlayerOrbitRequest::Freeze, stateMgr);
  } else {
    SetOrbitRequest(EPlayerOrbitRequest::Freeze, stateMgr);
  }

  AddMaterial(EMaterialTypes::Immovable, stateMgr);
  xa08_steamTextureId = steamTxtr;
  xa0c_iceTextureId = iceTxtr;
  CSfxHandle hnd = CSfxManager::SfxStart(sfx, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
  ApplySubmergedPitchBend(hnd);
  EndLandingControlFreeze();
}

bool CPlayer::GetFrozenState() const { return x750_frozenTimeout > 0.f; }

void CPlayer::UpdateFrozenState(const CFinalInput& input, CStateManager& mgr) {
  x750_frozenTimeout -= input.DeltaTime();
  if (x750_frozenTimeout > 0.f) {
    SetVisorSteam(0.7f, 6.f / 14.f, 1.f / 14.f, xa08_steamTextureId, false);
  } else {
    UnFreeze(mgr);
    return;
  }
  if (x258_movementState == EPlayerMovementState::OnGround ||
      x258_movementState == EPlayerMovementState::FallingMorphed) {
    Stop();
    ClearForcesAndTorques();
  }
  x7a0_visorSteam.Update(input.DeltaTime());

  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Morphed:
    x490_gun->ProcessInput(input, mgr);
    break;
  case EPlayerMorphBallState::Unmorphed:
  case EPlayerMorphBallState::Morphing:
  case EPlayerMorphBallState::Unmorphing:
    if (ControlMapper::GetPressInput(ControlMapper::ECommands::JumpOrBoost, input)) {
      if (x754_iceBreakJumps != 0) {
        /* Subsequent Breaks */
        CSfxHandle hnd = CSfxManager::SfxStart(SFXcrk_break_subsequent, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
        ApplySubmergedPitchBend(hnd);
      } else {
        /* Initial Break */
        CSfxHandle hnd = CSfxManager::SfxStart(SFXcrk_break_initial, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
        ApplySubmergedPitchBend(hnd);
      }
      x754_iceBreakJumps += 1;
      if (x754_iceBreakJumps > g_tweakPlayer->GetIceBreakJumpCount()) {
        g_GameState->SystemOptions().IncrementFrozenFpsCount();
        const CHUDMemoParms info(0.f, true, true, true);
        MP1::CSamusHud::DisplayHudMemo(u"", info);
        UnFreeze(mgr);
      }
    }
    break;
  }
}

void CPlayer::UpdateStepCameraZBias(float dt) {
  float newBias = GetTranslation().z() + GetUnbiasedEyeHeight();
  if (x258_movementState == EPlayerMovementState::OnGround && !IsMorphBallTransitioning()) {
    const float oldBias = newBias;
    if (!x9c5_31_stepCameraZBiasDirty) {
      const float delta = newBias - x9cc_stepCameraZBias;
      float newDelta = 5.f * dt;
      if (delta > 0.f) {
        if (delta > dt * x138_velocity.z() && delta > newDelta) {
          if (delta > GetStepUpHeight()) {
            newDelta += delta - GetStepUpHeight();
          }
          newBias = x9cc_stepCameraZBias + newDelta;
        }
      } else {
        if (delta < dt * x138_velocity.z() && delta < -newDelta) {
          if (delta < -GetStepDownHeight()) {
            newDelta += -delta - GetStepDownHeight();
          }
          newBias = x9cc_stepCameraZBias - newDelta;
        }
      }
    }
    x9c8_eyeZBias = newBias - oldBias;
  } else {
    x9c8_eyeZBias = 0.f;
  }
  x9cc_stepCameraZBias = newBias;
  x9c5_31_stepCameraZBiasDirty = false;
}

void CPlayer::UpdateWaterSurfaceCameraBias(CStateManager& mgr) {
  const TCastToConstPtr<CScriptWater> water = mgr.GetObjectById(xc4_fluidId);
  if (!water) {
    return;
  }

  const float waterZ = water->GetTriggerBoundsWR().max.z();
  const float biasToEyeDelta = GetEyePosition().z() - x9c8_eyeZBias;
  const float waterToDeltaDelta = biasToEyeDelta - waterZ;

  if (biasToEyeDelta >= waterZ && waterToDeltaDelta <= 0.25f) {
    x9c8_eyeZBias += waterZ + 0.25f - biasToEyeDelta;
  } else if (biasToEyeDelta < waterZ && waterToDeltaDelta >= -0.2f) {
    x9c8_eyeZBias += waterZ - 0.2f - biasToEyeDelta;
  }
}

void CPlayer::UpdateEnvironmentDamageCameraShake(float dt, CStateManager& mgr) {
  xa2c_damageLoopSfxDelayTicks = std::min(2, xa2c_damageLoopSfxDelayTicks + 1);

  if (xa10_envDmgCounter == 0) {
    return;
  }

  if (xa14_envDmgCameraShakeTimer == 0.f) {
    mgr.GetCameraManager()->AddCameraShaker(CCameraShakeData::BuildPhazonCameraShakeData(1.f, 0.075f), false);
  }
  xa14_envDmgCameraShakeTimer += dt;
  if (xa14_envDmgCameraShakeTimer > 2.f) {
    xa14_envDmgCameraShakeTimer = 0.f;
  }
}

void CPlayer::UpdatePhazonDamage(float dt, CStateManager& mgr) {
  if (x4_areaId == kInvalidAreaId) {
    return;
  }

  const CGameArea* area = mgr.GetWorld()->GetAreaAlways(x4_areaId);
  if (!area->IsPostConstructed()) {
    return;
  }

  bool touchingPhazon = false;
  EPhazonType phazonType;
  if (const CScriptAreaAttributes* attr = area->GetPostConstructed()->x10d8_areaAttributes) {
    phazonType = attr->GetPhazonType();
  } else {
    phazonType = EPhazonType::None;
  }

  if (phazonType == EPhazonType::Orange ||
      (!mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::PhazonSuit) && phazonType == EPhazonType::Blue)) {
    constexpr CMaterialFilter filter = CMaterialFilter::MakeInclude({EMaterialTypes::Phazon});
    if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
      touchingPhazon = x768_morphball->BallCloseToCollision(mgr, 2.9f, filter);
    } else {
      constexpr CMaterialList primMaterial(EMaterialTypes::Player, EMaterialTypes::Solid);
      const CCollidableSphere prim(
          zeus::CSphere(GetCollisionPrimitive()->CalculateAABox(x34_transform).center(), 4.25f), primMaterial);
      rstl::reserved_vector<TUniqueId, 1024> nearList;
      mgr.BuildColliderList(nearList, *this, prim.CalculateLocalAABox());
      if (CGameCollision::DetectStaticCollisionBoolean(mgr, prim, zeus::CTransform(), filter)) {
        touchingPhazon = true;
      } else {
        for (const auto& id : nearList) {
          if (const TCastToConstPtr<CPhysicsActor> act = mgr.GetObjectById(id)) {
            const CInternalCollisionStructure::CPrimDesc prim0(prim, filter, zeus::CTransform());
            const CInternalCollisionStructure::CPrimDesc prim1(
                *act->GetCollisionPrimitive(), CMaterialFilter::skPassEverything, act->GetPrimitiveTransform());
            if (CCollisionPrimitive::CollideBoolean(prim0, prim1)) {
              touchingPhazon = true;
              break;
            }
          }
        }
      }
    }
  }

  if (touchingPhazon) {
    xa18_phazonDamageLag += dt;
    xa18_phazonDamageLag = std::min(xa18_phazonDamageLag, 3.f);
    if (xa18_phazonDamageLag > 0.2f) {
      const float damage = (xa18_phazonDamageLag - 0.2f) / 3.f * 60.f * dt;
      const CDamageInfo dInfo(
          CWeaponMode(phazonType == EPhazonType::Orange ? EWeaponType::OrangePhazon : EWeaponType::Phazon), damage, 0.f,
          0.f);
      mgr.ApplyDamage(kInvalidUniqueId, GetUniqueId(), kInvalidUniqueId, dInfo,
                      CMaterialFilter::MakeIncludeExclude({EMaterialTypes::Solid}, {}), zeus::skZero3f);
    }
  } else {
    xa18_phazonDamageLag -= dt;
    xa18_phazonDamageLag = std::min(0.2f, xa18_phazonDamageLag);
    xa18_phazonDamageLag = std::max(0.f, xa18_phazonDamageLag);
  }

  xa1c_threatOverride = std::min(1.f, xa18_phazonDamageLag / 0.2f);
}

void CPlayer::ResetPlayerHintState() {
  x9c4_26_ = true;
  x9c4_27_canEnterMorphBall = true;
  x9c4_28_canLeaveMorphBall = true;
  x9c4_30_controlDirOverride = false;
  x9c6_24_extendTargetDistance = false;
  x9c6_26_outOfBallLookAtHint = false;
  x9c4_29_spiderBallControlXY = false;
  x9c6_29_disableInput = false;
  x9c7_25_outOfBallLookAtHintActor = false;
  x768_morphball->SetBoostEnabed(true);
  ResetControlDirectionInterpolation();
}

bool CPlayer::SetAreaPlayerHint(const CScriptPlayerHint& hint, CStateManager& mgr) {
  x9c4_26_ = (hint.GetOverrideFlags() & 0x1) != 0;
  x9c4_27_canEnterMorphBall = (hint.GetOverrideFlags() & 0x40) == 0;
  x9c4_28_canLeaveMorphBall = (hint.GetOverrideFlags() & 0x20) == 0;
  x9c4_30_controlDirOverride = (hint.GetOverrideFlags() & 0x2) != 0;
  if (x9c4_30_controlDirOverride) {
    x9d8_controlDirOverrideDir = hint.GetTransform().basis[1];
  }
  x9c6_24_extendTargetDistance = (hint.GetOverrideFlags() & 0x4) != 0;
  x9c6_26_outOfBallLookAtHint = (hint.GetOverrideFlags() & 0x8) != 0;
  x9c4_29_spiderBallControlXY = (hint.GetOverrideFlags() & 0x10) != 0;
  x9c6_29_disableInput = (hint.GetOverrideFlags() & 0x80) != 0;
  x9c7_25_outOfBallLookAtHintActor = (hint.GetOverrideFlags() & 0x4000) != 0;
  x768_morphball->SetBoostEnabed((hint.GetOverrideFlags() & 0x100) == 0);
  bool switchedVisor = false;
  if ((hint.GetOverrideFlags() & 0x200) != 0) {
    if (mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::CombatVisor)) {
      mgr.GetPlayerState()->StartTransitionToVisor(CPlayerState::EPlayerVisor::Combat);
    }
    switchedVisor = true;
  }
  if ((hint.GetOverrideFlags() & 0x400) != 0) {
    if (mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::ScanVisor)) {
      mgr.GetPlayerState()->StartTransitionToVisor(CPlayerState::EPlayerVisor::Scan);
    }
    switchedVisor = true;
  }
  if ((hint.GetOverrideFlags() & 0x800) != 0) {
    if (mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::ThermalVisor)) {
      mgr.GetPlayerState()->StartTransitionToVisor(CPlayerState::EPlayerVisor::Thermal);
    }
    switchedVisor = true;
  }
  if ((hint.GetOverrideFlags() & 0x1000) != 0) {
    if (mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::XRayVisor)) {
      mgr.GetPlayerState()->StartTransitionToVisor(CPlayerState::EPlayerVisor::XRay);
    }
    switchedVisor = true;
  }
  return switchedVisor;
}

void CPlayer::AddToPlayerHintRemoveList(TUniqueId id, CStateManager& mgr) {
  const TCastToConstPtr<CScriptPlayerHint> hint = mgr.ObjectById(id);
  if (!hint) {
    return;
  }

  for (const auto& existId : x93c_playerHintsToRemove) {
    if (id == existId) {
      return;
    }
  }

  if (x93c_playerHintsToRemove.size() != 32) {
    x93c_playerHintsToRemove.push_back(id);
  }
}

void CPlayer::AddToPlayerHintAddList(TUniqueId id, CStateManager& mgr) {
  const TCastToConstPtr<CScriptPlayerHint> hint = mgr.ObjectById(id);
  if (!hint) {
    return;
  }

  for (const auto& existId : x980_playerHintsToAdd) {
    if (id == existId) {
      return;
    }
  }

  if (x980_playerHintsToAdd.size() != 32) {
    x980_playerHintsToAdd.push_back(id);
  }
}

void CPlayer::DeactivatePlayerHint(TUniqueId id, CStateManager& mgr) {
  const TCastToPtr<CScriptPlayerHint> hint = mgr.ObjectById(id);
  if (!hint) {
    return;
  }

  for (const auto& existId : x93c_playerHintsToRemove) {
    if (id == existId) {
      return;
    }
  }

  if (x93c_playerHintsToRemove.size() != 32) {
    x93c_playerHintsToRemove.push_back(id);
    hint->ClearObjectList();
    hint->SetDeactivated();
  }
}

void CPlayer::UpdatePlayerHints(CStateManager& mgr) {
  bool removedHint = false;
  for (auto it = x838_playerHints.begin(); it != x838_playerHints.end();) {
    auto& p = *it;
    const TCastToConstPtr<CScriptPlayerHint> hint = mgr.ObjectById(p.second);
    if (!hint) {
      it = x838_playerHints.erase(it);
      removedHint = true;
    } else {
      ++it;
    }
  }

  bool needsNewHint = false;
  for (const auto& id : x93c_playerHintsToRemove) {
    for (auto it = x838_playerHints.begin(); it != x838_playerHints.end();) {
      if (it->second == id) {
        it = x838_playerHints.erase(it);
        if (id == x830_playerHint) {
          needsNewHint = true;
        }
        break;
      }
      ++it;
    }
  }
  x93c_playerHintsToRemove.clear();

  bool addedHint = false;
  for (const auto& id : x980_playerHintsToAdd) {
    if (const TCastToConstPtr<CScriptPlayerHint> hint = mgr.ObjectById(id)) {
      bool exists = false;
      for (auto& p : x838_playerHints) {
        if (p.second == id) {
          exists = true;
          break;
        }
      }
      if (!exists) {
        x838_playerHints.emplace_back(hint->GetPriority(), id);
        addedHint = true;
      }
    }
  }
  x980_playerHintsToAdd.clear();

  if (needsNewHint || addedHint || removedHint) {
    std::sort(x838_playerHints.begin(), x838_playerHints.end(),
              [](const auto& a, const auto& b) { return a.first < b.first; });

    if ((needsNewHint || removedHint) && x838_playerHints.empty()) {
      x830_playerHint = kInvalidUniqueId;
      x834_playerHintPriority = 1000;
      ResetPlayerHintState();
      return;
    }

    const CScriptPlayerHint* foundHint = nullptr;
    bool foundHintInArea = false;
    for (const auto& p : x838_playerHints) {
      if (const TCastToConstPtr<CScriptPlayerHint> hint = mgr.ObjectById(p.second)) {
        foundHint = hint.GetPtr();
        if (hint->GetAreaIdAlways() == mgr.GetNextAreaId()) {
          foundHintInArea = true;
          break;
        }
      }
    }

    if (!foundHintInArea) {
      x830_playerHint = kInvalidUniqueId;
      x834_playerHintPriority = 1000;
      ResetPlayerHintState();
    }

    if (foundHint != nullptr && foundHintInArea && x830_playerHint != foundHint->GetUniqueId()) {
      x830_playerHint = foundHint->GetUniqueId();
      x834_playerHintPriority = foundHint->GetPriority();
      if (SetAreaPlayerHint(*foundHint, mgr)) {
        DeactivatePlayerHint(x830_playerHint, mgr);
      }
    }
  }
}

void CPlayer::UpdateBombJumpStuff() {
  if (x9d0_bombJumpCount == 0) {
    return;
  }
  x9d4_bombJumpCheckDelayFrames -= 1;
  if (x9d4_bombJumpCheckDelayFrames > 0) {
    return;
  }

  zeus::CVector3f velFlat = x138_velocity;
  velFlat.z() = 0.f;
  if (x258_movementState == EPlayerMovementState::OnGround ||
      (velFlat.canBeNormalized() && velFlat.magnitude() > 6.f)) {
    x9d0_bombJumpCount = 0;
  }
}

void CPlayer::UpdateTransitionFilter(float dt, CStateManager& mgr) {
  if (x824_transitionFilterTimer <= 0.f) {
    mgr.GetCameraFilterPass(8).DisableFilter(0.f);
    return;
  }

  x824_transitionFilterTimer += dt;
  if (x824_transitionFilterTimer > 1.25f) {
    x824_transitionFilterTimer = 0.f;
    mgr.GetCameraFilterPass(8).DisableFilter(0.f);
    return;
  }

  if (x824_transitionFilterTimer < 0.95f) {
    return;
  }

  const float time = x824_transitionFilterTimer - 0.95f;
  zeus::CColor color(1.f, 0.87f, 0.54f, 1.f);
  if (time < 0.1f) {
    color.a() = 0.3f * time / 0.1f;
  } else if (time >= 0.15f) {
    color.a() = 1.f - zeus::clamp(-1.f, (time - 0.15f) / 0.15f, 1.f) * 0.3f;
  } else {
    color.a() = 0.3f;
  }

  mgr.GetCameraFilterPass(8).SetFilter(EFilterType::Add, EFilterShape::ScanLinesEven, 0.f, color, {});
}

void CPlayer::ResetControlDirectionInterpolation() {
  x9c6_25_interpolatingControlDir = false;
  x9f8_controlDirInterpTime = 0.f;
}

void CPlayer::SetControlDirectionInterpolation(float time) {
  x9c6_25_interpolatingControlDir = true;
  x9f8_controlDirInterpTime = 0.f;
  x9fc_controlDirInterpDur = time;
}

void CPlayer::UpdatePlayerControlDirection(float dt, CStateManager& mgr) {
  const zeus::CVector3f oldControlDir = x540_controlDir;
  const zeus::CVector3f oldControlDirFlat = x54c_controlDirFlat;
  CalculatePlayerControlDirection(mgr);

  if (!x9c6_25_interpolatingControlDir || x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
    return;
  }

  x9f8_controlDirInterpTime += dt;
  if (x9f8_controlDirInterpTime > x9fc_controlDirInterpDur) {
    x9f8_controlDirInterpTime = x9fc_controlDirInterpDur;
    ResetControlDirectionInterpolation();
  }

  const float t = zeus::clamp(-1.f, x9f8_controlDirInterpTime / x9fc_controlDirInterpDur, 1.f);
  x540_controlDir = zeus::CVector3f::lerp(oldControlDir, x540_controlDir, t);
  x54c_controlDirFlat = zeus::CVector3f::lerp(oldControlDirFlat, x540_controlDir, t);
}

void CPlayer::Think(float dt, CStateManager& mgr) {
  UpdateStepCameraZBias(dt);
  UpdateWaterSurfaceCameraBias(mgr);
  UpdateEnvironmentDamageCameraShake(dt, mgr);
  UpdatePhazonDamage(dt, mgr);
  UpdateFreeLook(dt);
  UpdatePlayerHints(mgr);

  if (x2b0_outOfWaterTicks < 2) {
    x2b0_outOfWaterTicks += 1;
  }

  x9c5_24_ = x9c4_24_visorChangeRequested;
  x9c4_31_inWaterMovement = x9c5_25_splashUpdated;
  x9c5_25_splashUpdated = false;
  UpdateBombJumpStuff();

  if (0.f < x288_startingJumpTimeout) {
    x288_startingJumpTimeout -= dt;
    if (0.f >= x288_startingJumpTimeout) {
      SetMoveState(EPlayerMovementState::ApplyJump, mgr);
    }
  }

  if (x2a0_ > 0.f) {
    x2a0_ += dt;
  }
  if (x774_samusVoiceTimeout > 0.f) {
    x774_samusVoiceTimeout -= dt;
  }
  if (0.f < x28c_sjTimer) {
    x28c_sjTimer -= dt;
  }

  x300_fallingTime += dt;
  if (x258_movementState == EPlayerMovementState::FallingMorphed && x300_fallingTime > 0.4f) {
    SetMoveState(EPlayerMovementState::ApplyJump, mgr);
  }

  if (x570_immuneTimer > 0.f) {
    x570_immuneTimer -= dt;
  }

  Update(dt, mgr);
  UpdateTransitionFilter(dt, mgr);
  CalculatePlayerMovementDirection(dt);
  UpdatePlayerControlDirection(dt, mgr);

#if 0
    if (g_factoryManager == 4) {
        x2b0_ = 0;
    } else {
        x2ac_surfaceRestraint = ESurfaceRestraints::Normal;
    }
#endif

  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed && x9c5_27_camSubmerged &&
      mgr.GetCameraManager()->GetFluidCounter() == 0) {
    if (const auto* water = GetVisorRunoffEffect(mgr)) {
      if (const auto& effect = water->GetVisorRunoffEffect()) {
        auto* sheets = new CHUDBillboardEffect(
            *effect, {}, mgr.AllocateUniqueId(), true, "WaterSheets", CHUDBillboardEffect::GetNearClipDistance(mgr),
            CHUDBillboardEffect::GetScaleForPOV(mgr), zeus::skWhite, zeus::skOne3f, zeus::skZero3f);
        mgr.AddObject(sheets);
      }
      CSfxHandle hnd = CSfxManager::SfxStart(water->GetVisorRunoffSfx(), 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
      ApplySubmergedPitchBend(hnd);
    }
  }
  x9c5_27_camSubmerged = mgr.GetCameraManager()->GetFluidCounter() != 0;

  if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
    if (std::fabs(x34_transform.basis[0].z()) > FLT_EPSILON || std::fabs(x34_transform.basis[1].z()) > FLT_EPSILON) {
      const zeus::CVector3f backupTranslation = GetTranslation();
      const zeus::CVector3f lookDirFlat(x34_transform.basis[1].x(), x34_transform.basis[1].y(), 0.f);
      if (lookDirFlat.canBeNormalized()) {
        SetTransform(zeus::lookAt(zeus::skZero3f, lookDirFlat.normalized()));
      } else {
        SetTransform(zeus::CTransform());
      }
      SetTranslation(backupTranslation);
    }
  }

  x794_lastVelocity = x138_velocity;
}

void CPlayer::PreThink(float dt, CStateManager& mgr) {
  x558_wasDamaged = false;
  x55c_damageAmt = 0.f;
  x560_prevDamageAmt = 0.f;
  x564_damageLocation = zeus::skZero3f;
  xa04_preThinkDt = dt;
}

void CPlayer::AcceptScriptMsg(EScriptObjectMessage msg, TUniqueId sender, CStateManager& mgr) {
  switch (msg) {
  case EScriptObjectMessage::OnFloor:
    if (x258_movementState != EPlayerMovementState::OnGround && x2f8_morphBallState != EPlayerMorphBallState::Morphed &&
        x300_fallingTime > 0.3f) {
      if (x258_movementState != EPlayerMovementState::Falling) {
        float hardThres = 30.f * 2.f * -g_tweakPlayer->GetNormalGravAccel();
        hardThres = (hardThres != 0.f) ? hardThres * (1.f / std::sqrt(hardThres)) : 0.f;
        const float landVol = zeus::clamp(95.f, 1.6f * -x794_lastVelocity.z() + 95.f, 127.f) / 127.f;
        u16 landSfx;
        if (-x794_lastVelocity.z() < hardThres) {
          landSfx = GetMaterialSoundUnderPlayer(mgr, skPlayerLandSfxSoft.data(), skPlayerLandSfxSoft.size(), 0xffff);
        } else {
          landSfx = GetMaterialSoundUnderPlayer(mgr, skPlayerLandSfxHard.data(), skPlayerLandSfxHard.size(), 0xffff);
          StartSamusVoiceSfx(SFXsam_voxland_02, 1.f, 5);
          x55c_damageAmt = 0.f;
          x560_prevDamageAmt = 10.f;
          x564_damageLocation = x34_transform.origin;
          x558_wasDamaged = true;
          mgr.GetCameraManager()->AddCameraShaker(CCameraShakeData::BuildLandingCameraShakeData(0.3f, 1.25f), false);
          StartLandingControlFreeze();
        }
        CSfxHandle handle = CSfxManager::SfxStart(landSfx, landVol, 0.f, false, 0x7f, false, kInvalidAreaId);
        ApplySubmergedPitchBend(handle);

        float rumbleMag = -x794_lastVelocity.z() / 110.f;
        if (rumbleMag > 0.f) {
          if (std::fabs(rumbleMag) > 0.8f) {
            rumbleMag = (rumbleMag > 0.f) ? 0.8f : -0.8f;
          }
          mgr.GetRumbleManager().Rumble(mgr, ERumbleFxId::PlayerLand, rumbleMag, ERumblePriority::One);
        }

        x2a0_ = 0.f;
      }
    } else if (x258_movementState != EPlayerMovementState::OnGround &&
               x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
      if (x138_velocity.z() < -40.f && !x768_morphball->GetIsInHalfPipeMode() &&
          x258_movementState == EPlayerMovementState::ApplyJump && x300_fallingTime > 0.75f) {
        SetCoefficientOfRestitutionModifier(0.2f);
      }
      x768_morphball->StartLandingSfx();
      if (x138_velocity.z() < -5.f) {
        float rumbleMag = -x138_velocity.z() / 110.f * 0.5f;
        if (std::fabs(rumbleMag) > 0.8f) {
          rumbleMag = (rumbleMag > 0.f) ? 0.8f : -0.8f;
        }
        mgr.GetRumbleManager().Rumble(mgr, ERumbleFxId::PlayerLand, rumbleMag, ERumblePriority::One);
        x2a0_ = 0.f;
      }
      if (x138_velocity.z() < -30.f) {
        float rumbleMag = -x138_velocity.z() / 110.f;
        if (std::fabs(rumbleMag) > 0.8f) {
          rumbleMag = (rumbleMag > 0.f) ? 0.8f : -0.8f;
        }
        mgr.GetRumbleManager().Rumble(mgr, ERumbleFxId::PlayerLand, rumbleMag, ERumblePriority::One);
        x2a0_ = 0.f;
      }
    }
    x300_fallingTime = 0.f;
    SetMoveState(EPlayerMovementState::OnGround, mgr);
    break;
  case EScriptObjectMessage::Falling:
    if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
      if (x768_morphball->GetSpiderBallState() == CMorphBall::ESpiderBallState::Active) {
        break;
      }
    }
    if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
      SetMoveState(EPlayerMovementState::Falling, mgr);
    } else if (x258_movementState == EPlayerMovementState::OnGround) {
      SetMoveState(EPlayerMovementState::FallingMorphed, mgr);
    }
    break;
  case EScriptObjectMessage::LandOnNotFloor:
    if (x2f8_morphBallState == EPlayerMorphBallState::Morphed &&
        x768_morphball->GetSpiderBallState() == CMorphBall::ESpiderBallState::Active &&
        x258_movementState != EPlayerMovementState::ApplyJump) {
      SetMoveState(EPlayerMovementState::ApplyJump, mgr);
    }
    break;
  case EScriptObjectMessage::OnIceSurface:
    x2ac_surfaceRestraint = ESurfaceRestraints::Ice;
    break;
  case EScriptObjectMessage::OnMudSlowSurface:
    x2ac_surfaceRestraint = ESurfaceRestraints::Organic;
    break;
  case EScriptObjectMessage::OnNormalSurface:
    x2ac_surfaceRestraint = ESurfaceRestraints::Normal;
    break;
  case EScriptObjectMessage::InSnakeWeed:
    x2ac_surfaceRestraint = ESurfaceRestraints::Shrubbery;
    break;
  case EScriptObjectMessage::AddSplashInhabitant: {
    SetInFluid(true, sender);
    UpdateSubmerged(mgr);
    const CRayCastResult result =
        mgr.RayStaticIntersection(x34_transform.origin, zeus::skDown, 0.5f * GetEyeHeight(), SolidMaterialFilter);
    if (result.IsInvalid()) {
      SetVelocityWR(x138_velocity * 0.095f);
      xfc_constantForce *= zeus::CVector3f(0.095f);
    }
    break;
  }
  case EScriptObjectMessage::UpdateSplashInhabitant:
    UpdateSubmerged(mgr);
    if (CheckSubmerged() && !mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::GravitySuit)) {
      if (const TCastToConstPtr<CScriptWater> water = mgr.ObjectById(xc4_fluidId)) {
        switch (water->GetFluidPlane().GetFluidType()) {
        case EFluidType::NormalWater:
          x2b0_outOfWaterTicks = 0;
          break;
        case EFluidType::Lava:
        case EFluidType::ThickLava:
          x2ac_surfaceRestraint = ESurfaceRestraints::Lava;
          break;
        case EFluidType::PoisonWater:
          x2b0_outOfWaterTicks = 0;
          break;
        case EFluidType::PhazonFluid:
          x2ac_surfaceRestraint = ESurfaceRestraints::Phazon;
          break;
        default:
          break;
        }
      }
    }
    x9c5_25_splashUpdated = true;
    break;
  case EScriptObjectMessage::RemoveSplashInhabitant:
    SetInFluid(false, kInvalidUniqueId);
    UpdateSubmerged(mgr);
    break;
  case EScriptObjectMessage::ProjectileCollide:
    x378_orbitPreventionTimer = g_tweakPlayer->GetOrbitPreventionTime();
    SetOrbitRequest(EPlayerOrbitRequest::ProjectileCollide, mgr);
    break;
  case EScriptObjectMessage::AddPlatformRider:
    x82e_ridingPlatform = sender;
    break;
  case EScriptObjectMessage::Damage:
    if (const TCastToConstPtr<CEnergyProjectile> energ = mgr.ObjectById(sender)) {
      if (True(energ->GetAttribField() & EProjectileAttrib::StaticInterference)) {
        mgr.GetPlayerState()->GetStaticInterference().AddSource(GetUniqueId(), 0.3f, energ->GetInterferenceDuration());
      }
    }
    break;
  case EScriptObjectMessage::Deleted:
    mgr.GetPlayerState()->ResetVisor();
    x730_transitionModels.clear();
    break;
  default:
    break;
  }

  x490_gun->AcceptScriptMsg(msg, sender, mgr);
  x768_morphball->AcceptScriptMsg(msg, sender, mgr);
  CActor::AcceptScriptMsg(msg, sender, mgr);
}

void CPlayer::SetVisorSteam(float targetAlpha, float alphaInDur, float alphaOutDur, CAssetId txtr,
                            bool affectsThermal) {
  x7a0_visorSteam.SetSteam(targetAlpha, alphaInDur, alphaOutDur, txtr, affectsThermal);
}

void CPlayer::UpdateFootstepSounds(const CFinalInput& input, CStateManager& mgr, float dt) {
  if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed || x258_movementState != EPlayerMovementState::OnGround ||
      x3dc_inFreeLook || x3dd_lookButtonHeld) {
    return;
  }

  float sfxVol = 1.f;
  x78c_footstepSfxTimer += dt;
  float turn = TurnInput(input);
  const float forward = std::fabs(ForwardInput(input, turn));
  turn = std::fabs(turn);
  float sfxDelay = 0.f;
  if (forward > 0.05f || x304_orbitState != EPlayerOrbitState::NoOrbit) {
    const float vel = std::min(1.f, x138_velocity.magnitude() / GetActualFirstPersonMaxVelocity(dt));
    if (vel > 0.05f) {
      sfxDelay = -0.475f * vel + 0.85f;
      if (x790_footstepSfxSel == EFootstepSfx::None) {
        x790_footstepSfxSel = EFootstepSfx::Left;
      }
    } else {
      x78c_footstepSfxTimer = 0.f;
      x790_footstepSfxSel = EFootstepSfx::None;
    }

    sfxVol = 0.3f * vel + 0.7f;
  } else if (turn > 0.05f) {
    if (x790_footstepSfxSel == EFootstepSfx::Left) {
      sfxDelay = -0.813f * turn + 1.f;
    } else {
      sfxDelay = -2.438f * turn + 3.f;
    }
    if (x790_footstepSfxSel == EFootstepSfx::None) {
      x790_footstepSfxSel = EFootstepSfx::Left;
      sfxDelay = x78c_footstepSfxTimer;
    }
    sfxVol = 0.75f;
  } else {
    x78c_footstepSfxTimer = 0.f;
    x790_footstepSfxSel = EFootstepSfx::None;
  }

  if (x790_footstepSfxSel != EFootstepSfx::None && x78c_footstepSfxTimer > sfxDelay) {
    static float EarHeight = GetEyeHeight() - 0.1f;
    if (xe6_24_fluidCounter != 0 && x828_distanceUnderWater > 0.f && x828_distanceUnderWater < EarHeight) {
      if (x82c_inLava) {
        if (x790_footstepSfxSel == EFootstepSfx::Left) {
          CSfxHandle hnd = CSfxManager::SfxStart(SFXlav_wlklava_00, sfxVol, 0.f, true, 0x7f, false, kInvalidAreaId);
          ApplySubmergedPitchBend(hnd);
        } else {
          CSfxHandle hnd = CSfxManager::SfxStart(SFXlav_wlklava_01, sfxVol, 0.f, true, 0x7f, false, kInvalidAreaId);
          ApplySubmergedPitchBend(hnd);
        }
      } else {
        if (x790_footstepSfxSel == EFootstepSfx::Left) {
          CSfxHandle hnd = CSfxManager::SfxStart(SFXsam_wlkwater_00, sfxVol, 0.f, true, 0x7f, false, kInvalidAreaId);
          ApplySubmergedPitchBend(hnd);
        } else {
          CSfxHandle hnd = CSfxManager::SfxStart(SFXsam_wlkwater_01, sfxVol, 0.f, true, 0x7f, false, kInvalidAreaId);
          ApplySubmergedPitchBend(hnd);
        }
      }
    } else {
      u16 sfx;
      if (x790_footstepSfxSel == EFootstepSfx::Left) {
        sfx = GetMaterialSoundUnderPlayer(mgr, skLeftStepSounds.data(), skLeftStepSounds.size(), 0xffff);
      } else {
        sfx = GetMaterialSoundUnderPlayer(mgr, skRightStepSounds.data(), skRightStepSounds.size(), 0xffff);
      }
      CSfxHandle hnd = CSfxManager::SfxStart(sfx, sfxVol, 0.f, true, 0x7f, false, kInvalidAreaId);
      ApplySubmergedPitchBend(hnd);
    }

    x78c_footstepSfxTimer = 0.f;
    if (x790_footstepSfxSel == EFootstepSfx::Left) {
      x790_footstepSfxSel = EFootstepSfx::Right;
    } else {
      x790_footstepSfxSel = EFootstepSfx::Left;
    }
  }
}

u16 CPlayer::GetMaterialSoundUnderPlayer(const CStateManager& mgr, const u16* table, size_t length, u16 defId) const {
  u16 ret = defId;
  zeus::CAABox aabb = GetBoundingBox();
  aabb.accumulateBounds(x34_transform.origin + zeus::skDown);
  rstl::reserved_vector<TUniqueId, 1024> nearList;
  mgr.BuildNearList(nearList, aabb, SolidMaterialFilter, nullptr);
  TUniqueId collideId = kInvalidUniqueId;
  const CRayCastResult result =
      mgr.RayWorldIntersection(collideId, x34_transform.origin, zeus::skDown, 1.5f, SolidMaterialFilter, nearList);
  if (result.IsValid()) {
    ret = SfxIdFromMaterial(result.GetMaterial(), table, length, defId);
  }
  return ret;
}

u16 CPlayer::SfxIdFromMaterial(const CMaterialList& mat, const u16* idList, size_t tableLen, u16 defId) {
  u16 id = defId;
  for (size_t i = 0; i < tableLen; ++i) {
    if (mat.HasMaterial(EMaterialTypes(i)) && idList[i] != 0xFFFF) {
      id = idList[i];
    }
  }
  return id;
}

void CPlayer::UpdateCrosshairsState(const CFinalInput& input) {
  x9c4_25_showCrosshairs = ControlMapper::GetDigitalInput(ControlMapper::ECommands::ShowCrosshairs, input);
}

void CPlayer::UpdateVisorTransition(float dt, CStateManager& mgr) {
  if (mgr.GetPlayerState()->GetIsVisorTransitioning()) {
    mgr.GetPlayerState()->UpdateVisorTransition(dt);
  }
}

void CPlayer::UpdateVisorState(const CFinalInput& input, float dt, CStateManager& mgr) {
  x7a0_visorSteam.Update(dt);
  if (x7a0_visorSteam.AffectsThermal()) {
    mgr.SetThermalColdScale2(mgr.GetThermalColdScale2() + x7a0_visorSteam.GetAlpha());
  }

  if (x304_orbitState == EPlayerOrbitState::Grapple ||
      TCastToPtr<CScriptGrapplePoint>(mgr.ObjectById(x310_orbitTargetId)) ||
      x2f8_morphBallState != EPlayerMorphBallState::Unmorphed || mgr.GetPlayerState()->GetIsVisorTransitioning() ||
      x3a8_scanState != EPlayerScanState::NotScanning) {
    return;
  }

  if (mgr.GetPlayerState()->GetTransitioningVisor() == CPlayerState::EPlayerVisor::Scan &&
      (ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input) ||
       ControlMapper::GetDigitalInput(ControlMapper::ECommands::MissileOrPowerBomb, input)) &&
      mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::CombatVisor)) {
    mgr.GetPlayerState()->StartTransitionToVisor(CPlayerState::EPlayerVisor::Combat);
    DrawGun(mgr);
  }

  for (size_t i = 0; i < skVisorToItemMapping.size(); ++i) {
    const auto mapping = skVisorToItemMapping[i];

    if (mgr.GetPlayerState()->HasPowerUp(mapping.first) && ControlMapper::GetPressInput(mapping.second, input)) {
      x9c4_24_visorChangeRequested = true;
      const auto visor = CPlayerState::EPlayerVisor(i);

      if (mgr.GetPlayerState()->GetTransitioningVisor() != visor) {
        mgr.GetPlayerState()->StartTransitionToVisor(visor);
        if (visor == CPlayerState::EPlayerVisor::Scan) {
          HolsterGun(mgr);
        } else {
          DrawGun(mgr);
        }
      }
    }
  }
}

void CPlayer::UpdateGunState(const CFinalInput& input, CStateManager& mgr) {
  switch (x498_gunHolsterState) {
  case EGunHolsterState::Drawn: {
    bool needsHolster = false;
    if (g_tweakPlayer->GetGunButtonTogglesHolster()) {
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::ToggleHolster, input)) {
        needsHolster = true;
      }
      if (!ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input) &&
          !ControlMapper::GetDigitalInput(ControlMapper::ECommands::MissileOrPowerBomb, input) &&
          g_tweakPlayer->GetGunNotFiringHolstersGun()) {
        x49c_gunHolsterRemTime -= input.DeltaTime();
        if (x49c_gunHolsterRemTime <= 0.f) {
          needsHolster = true;
        }
      }
    } else {
      if (!ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input) &&
          !ControlMapper::GetDigitalInput(ControlMapper::ECommands::MissileOrPowerBomb, input) &&
          x490_gun->IsFidgeting()) {
        if (g_tweakPlayer->GetGunNotFiringHolstersGun()) {
          x49c_gunHolsterRemTime -= input.DeltaTime();
        }
      } else {
        x49c_gunHolsterRemTime = g_tweakPlayerGun->GetGunNotFiringTime();
      }
    }

    if (needsHolster) {
      HolsterGun(mgr);
    }
    break;
  }
  case EGunHolsterState::Drawing: {
    if (x49c_gunHolsterRemTime > 0.f) {
      x49c_gunHolsterRemTime -= input.DeltaTime();
    } else {
      x498_gunHolsterState = EGunHolsterState::Drawn;
      x49c_gunHolsterRemTime = g_tweakPlayerGun->GetGunNotFiringTime();
    }
    break;
  }
  case EGunHolsterState::Holstered: {
    bool needsDraw = false;
    if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input) ||
        ControlMapper::GetDigitalInput(ControlMapper::ECommands::MissileOrPowerBomb, input) ||
        x3b8_grappleState == EGrappleState::None ||
        (g_tweakPlayer->GetGunButtonTogglesHolster() &&
         ControlMapper::GetPressInput(ControlMapper::ECommands::ToggleHolster, input))) {
      needsDraw = true;
    }

    if (x3b8_grappleState == EGrappleState::None &&
        (mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan ||
         mgr.GetPlayerState()->GetTransitioningVisor() == CPlayerState::EPlayerVisor::Scan)) {
      needsDraw = false;
    }

    if (needsDraw) {
      DrawGun(mgr);
    }
    break;
  }
  case EGunHolsterState::Holstering:
    if (x49c_gunHolsterRemTime > 0.f) {
      x49c_gunHolsterRemTime -= input.DeltaTime();
    } else {
      x498_gunHolsterState = EGunHolsterState::Holstered;
    }
    break;
  }
}

void CPlayer::ResetGun(CStateManager& mgr) {
  x498_gunHolsterState = EGunHolsterState::Holstered;
  x49c_gunHolsterRemTime = 0.f;
  x490_gun->CancelFiring(mgr);
  ResetAimTargetPrediction(kInvalidUniqueId);
}

void CPlayer::UpdateArmAndGunTransforms(float dt, CStateManager& mgr) {
  zeus::CVector3f grappleOffset;
  zeus::CVector3f gunOffset;
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed) {
    gunOffset = {0.f, 0.f, 0.6f};
  } else {
    gunOffset = g_tweakPlayerGun->GetGunPosition();
    grappleOffset =
        x490_gun->GetGrappleArm().IsArmMoving() ? zeus::skZero3f : g_tweakPlayerGun->GetGrapplingArmPosition();
    gunOffset.z() += GetEyeHeight();
    grappleOffset.z() += GetEyeHeight();
  }

  UpdateGunTransform(gunOffset + x76c_cameraBob->GetGunBobTransformation().origin, mgr);
  UpdateGrappleArmTransform(grappleOffset, mgr, dt);
}

void CPlayer::ForceGunOrientation(const zeus::CTransform& xf, CStateManager& mgr) {
  ResetGun(mgr);
  x530_gunDir = xf.basis[1];
  x490_gun->SetTransform(xf);
  UpdateArmAndGunTransforms(0.01f, mgr);
}

void CPlayer::UpdateCameraState(CStateManager& mgr) { UpdateCinematicState(mgr); }

void CPlayer::UpdateDebugCamera(CStateManager& mgr) {
  // Empty
}

void CPlayer::UpdateCameraTimers(float dt, const CFinalInput& input) {
  if (x3dc_inFreeLook || x3dd_lookButtonHeld) {
    x294_jumpCameraTimer = 0.f;
    x29c_fallCameraTimer = 0.f;
    return;
  }

  if (g_tweakPlayer->GetFiringCancelsCameraPitch()) {
    if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input) ||
        ControlMapper::GetDigitalInput(ControlMapper::ECommands::MissileOrPowerBomb, input)) {
      if (x288_startingJumpTimeout > 0.f) {
        x2a4_cancelCameraPitch = true;
        return;
      }
    }
  }

  if (ControlMapper::GetPressInput(ControlMapper::ECommands::JumpOrBoost, input)) {
    ++x298_jumpPresses;
  }

  if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::JumpOrBoost, input) && x294_jumpCameraTimer > 0.f &&
      !x2a4_cancelCameraPitch && x298_jumpPresses <= 2) {
    x294_jumpCameraTimer += dt;
  }

  if (x29c_fallCameraTimer > 0.f && !x2a4_cancelCameraPitch) {
    x29c_fallCameraTimer += dt;
  }
}

void CPlayer::UpdateMorphBallState(float dt, const CFinalInput& input, CStateManager& mgr) {
  if (!ControlMapper::GetPressInput(ControlMapper::ECommands::Morph, input)) {
    return;
  }

  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Unmorphed:
    if (mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::MorphBall) && CanEnterMorphBallState(mgr, 0.f)) {
      x574_morphTime = 0.f;
      x578_morphDuration = 1.f;
      TransitionToMorphBallState(dt, mgr);
    } else {
      CSfxHandle hnd = CSfxManager::SfxStart(SFXwpn_invalid_action, 1.f, 0.f, true, 0x7f, false, kInvalidAreaId);
      ApplySubmergedPitchBend(hnd);
    }
    break;
  case EPlayerMorphBallState::Morphed: {
    zeus::CVector3f posDelta;
    if (CanLeaveMorphBallState(mgr, posDelta)) {
      SetTranslation(x34_transform.origin + posDelta);
      x574_morphTime = 0.f;
      x578_morphDuration = 1.f;
      TransitionFromMorphBallState(mgr);
    } else {
      CSfxHandle hnd = CSfxManager::SfxStart(SFXwpn_invalid_action, 1.f, 0.f, true, 0x7f, false, kInvalidAreaId);
      ApplySubmergedPitchBend(hnd);
    }
    break;
  }
  default:
    break;
  }
}

CFirstPersonCamera& CPlayer::GetFirstPersonCamera(CStateManager& mgr) {
  return *mgr.GetCameraManager()->GetFirstPersonCamera();
}

void CPlayer::UpdateGunTransform(const zeus::CVector3f& gunPos, CStateManager& mgr) {
  const float eyeHeight = GetEyeHeight();
  const zeus::CTransform camXf = mgr.GetCameraManager()->GetCurrentCameraTransform(mgr);
  zeus::CTransform gunXf = camXf;

  zeus::CVector3f viewGunPos;
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphing) {
    viewGunPos = camXf * (gunPos - zeus::CVector3f(0.f, 0.f, eyeHeight));
  } else {
    viewGunPos = camXf.rotate(gunPos - zeus::CVector3f(0.f, 0.f, eyeHeight)) + GetEyePosition();
  }

  const zeus::CUnitVector3f rightDir(gunXf.basis[0]);
  gunXf.origin = viewGunPos;

  switch (x498_gunHolsterState) {
  case EGunHolsterState::Drawing: {
    const float liftAngle = zeus::clamp(-1.f, x49c_gunHolsterRemTime / 0.45f, 1.f);
    if (liftAngle > 0.01f) {
      gunXf = zeus::CQuaternion::fromAxisAngle(rightDir, -liftAngle * g_tweakPlayerGun->GetFixedVerticalAim())
                  .toTransform() *
              camXf.getRotation();
      gunXf.origin = viewGunPos;
    }
    break;
  }
  case EGunHolsterState::Holstered: {
    gunXf = zeus::CQuaternion::fromAxisAngle(rightDir, -g_tweakPlayerGun->GetFixedVerticalAim()).toTransform() *
            camXf.getRotation();
    gunXf.origin = viewGunPos;
    break;
  }
  case EGunHolsterState::Holstering: {
    float liftAngle = 1.f - zeus::clamp(-1.f, x49c_gunHolsterRemTime / g_tweakPlayerGun->GetGunHolsterTime(), 1.f);
    if (x2f8_morphBallState == EPlayerMorphBallState::Morphing) {
      liftAngle = 1.f - zeus::clamp(-1.f, x49c_gunHolsterRemTime / 0.1f, 1.f);
    }
    if (liftAngle > 0.01f) {
      gunXf = zeus::CQuaternion::fromAxisAngle(rightDir, -liftAngle * g_tweakPlayerGun->GetFixedVerticalAim())
                  .toTransform() *
              camXf.getRotation();
      gunXf.origin = viewGunPos;
    }
    break;
  }
  default:
    break;
  }

  x490_gun->SetTransform(gunXf);
  UpdateAimTargetPrediction(gunXf, mgr);
  UpdateAssistedAiming(gunXf, mgr);
}

void CPlayer::UpdateAssistedAiming(const zeus::CTransform& xf, const CStateManager& mgr) {
  zeus::CTransform assistXf = xf;
  const TCastToConstPtr<CActor> target = mgr.GetObjectById(x3f4_aimTarget);

  if (target) {
    zeus::CVector3f gunToTarget = x480_assistedTargetAim - xf.origin;
    zeus::CVector3f gunToTargetFlat = gunToTarget;
    gunToTargetFlat.z() = 0.f;
    const float gunToTargetFlatMag = gunToTargetFlat.magnitude();
    zeus::CVector3f gunDirFlat = xf.basis[1];
    gunDirFlat.z() = 0.f;
    const float gunDirFlatMag = gunDirFlat.magnitude();
    if (gunToTargetFlat.canBeNormalized() && gunDirFlat.canBeNormalized()) {
      gunToTargetFlat = gunToTargetFlat / gunToTargetFlatMag;
      gunDirFlat = gunDirFlat / gunDirFlatMag;
      float vAngleDelta = std::atan2(gunToTarget.z(), gunToTargetFlatMag) - std::atan2(xf.basis[1].z(), gunDirFlatMag);
      bool hasVAngleDelta = true;
      if (!x9c6_27_aimingAtProjectile && std::fabs(vAngleDelta) > g_tweakPlayer->GetAimAssistVerticalAngle()) {
        if (g_tweakPlayer->GetAssistedAimingIgnoreVertical()) {
          vAngleDelta = 0.f;
          hasVAngleDelta = false;
        } else if (vAngleDelta > 0.f) {
          vAngleDelta = g_tweakPlayer->GetAimAssistVerticalAngle();
        } else {
          vAngleDelta = -g_tweakPlayer->GetAimAssistVerticalAngle();
        }
      }

      const bool targetToLeft = gunDirFlat.cross(gunToTargetFlat).z() > 0.f;
      float hAngleDelta = std::acos(zeus::clamp(-1.f, gunDirFlat.dot(gunToTargetFlat), 1.f));
      bool hasHAngleDelta = true;
      if (!x9c6_27_aimingAtProjectile && std::fabs(hAngleDelta) > g_tweakPlayer->GetAimAssistHorizontalAngle()) {
        hAngleDelta = g_tweakPlayer->GetAimAssistHorizontalAngle();
        if (g_tweakPlayer->GetAssistedAimingIgnoreHorizontal()) {
          hAngleDelta = 0.f;
          hasHAngleDelta = false;
        }
      }

      if (targetToLeft)
        hAngleDelta = -hAngleDelta;

      if (!hasVAngleDelta || !hasHAngleDelta) {
        vAngleDelta = 0.f;
        hAngleDelta = 0.f;
      }

      gunToTarget.x() = std::sin(hAngleDelta) * std::cos(vAngleDelta);
      gunToTarget.y() = std::cos(hAngleDelta) * std::cos(vAngleDelta);
      gunToTarget.z() = std::sin(vAngleDelta);
      gunToTarget = xf.rotate(gunToTarget);
      assistXf = zeus::lookAt(zeus::skZero3f, gunToTarget, zeus::skUp);
    }
  }

  x490_gun->SetAssistAimTransform(assistXf);
}

void CPlayer::UpdateAimTargetPrediction(const zeus::CTransform& xf, const CStateManager& mgr) {
  if (x3f4_aimTarget == kInvalidUniqueId) {
    return;
  }

  const TCastToConstPtr<CActor> target = mgr.GetObjectById(x3f4_aimTarget);
  if (!target) {
    return;
  }

  x9c6_27_aimingAtProjectile = TCastToConstPtr<CGameProjectile>(target.GetPtr()).IsValid();
  const zeus::CVector3f instantTarget = target->GetAimPosition(mgr, 0.f);
  const zeus::CVector3f gunToTarget = instantTarget - xf.origin;
  const float timeToTarget = gunToTarget.magnitude() / x490_gun->GetBeamVelocity();
  const zeus::CVector3f predictTarget = target->GetAimPosition(mgr, timeToTarget);
  const zeus::CVector3f predictOffset = predictTarget - instantTarget;
  x3f8_targetAimPosition = instantTarget;

  if (predictOffset.magnitude() < 0.1f) {
    x404_aimTargetAverage.AddValue(zeus::skZero3f);
  } else {
    x404_aimTargetAverage.AddValue(predictOffset);
  }

  if (auto avg = x404_aimTargetAverage.GetAverage()) {
    x480_assistedTargetAim = instantTarget + *avg;
  } else {
    x480_assistedTargetAim = predictTarget;
  }
}

void CPlayer::ResetAimTargetPrediction(TUniqueId target) {
  if (target == kInvalidUniqueId || x3f4_aimTarget != target) {
    x404_aimTargetAverage.Clear();
  }
  x3f4_aimTarget = target;
}

void CPlayer::DrawGun(CStateManager& mgr) {
  if (x498_gunHolsterState != EGunHolsterState::Holstered || InGrappleJumpCooldown()) {
    return;
  }

  x498_gunHolsterState = EGunHolsterState::Drawing;
  x49c_gunHolsterRemTime = 0.45f;
  x490_gun->ResetIdle(mgr);
}

void CPlayer::HolsterGun(CStateManager& mgr) {
  if (x498_gunHolsterState == EGunHolsterState::Holstered || x498_gunHolsterState == EGunHolsterState::Holstering) {
    return;
  }

  const float time =
      x2f8_morphBallState == EPlayerMorphBallState::Morphing ? 0.1f : g_tweakPlayerGun->GetGunHolsterTime();
  if (x498_gunHolsterState == EGunHolsterState::Drawing) {
    x49c_gunHolsterRemTime = time * (1.f - x49c_gunHolsterRemTime / 0.45f);
  } else {
    x49c_gunHolsterRemTime = time;
  }

  x498_gunHolsterState = EGunHolsterState::Holstering;
  x490_gun->CancelFiring(mgr);
  ResetAimTargetPrediction(kInvalidUniqueId);
}

bool CPlayer::IsMorphBallTransitioning() const {
  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Morphing:
  case EPlayerMorphBallState::Unmorphing:
    return true;
  default:
    return false;
  }
}

void CPlayer::UpdateGrappleArmTransform(const zeus::CVector3f& offset, CStateManager& mgr, float dt) {
  zeus::CTransform armXf = x34_transform;
  zeus::CVector3f armPosition = x34_transform.rotate(offset) + x34_transform.origin;
  armXf.origin = armPosition;

  if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed) {
    x490_gun->GetGrappleArm().SetTransform(armXf);
  } else if (!x490_gun->GetGrappleArm().IsArmMoving()) {
    zeus::CVector3f lookDir = x34_transform.basis[1];
    zeus::CVector3f armToTarget = x490_gun->GetGrappleArm().GetTransform().basis[1];
    if (lookDir.canBeNormalized()) {
      lookDir.normalize();
      if (x3b8_grappleState != EGrappleState::None) {
        if (const TCastToConstPtr<CActor> target = mgr.ObjectById(x310_orbitTargetId)) {
          armToTarget = target->GetTranslation() - armPosition;
          zeus::CVector3f armToTargetFlat = armToTarget;
          armToTargetFlat.z() = 0.f;
          if (armToTarget.canBeNormalized()) {
            armToTarget.normalize();
          }
          if (armToTargetFlat.canBeNormalized() && x3b8_grappleState != EGrappleState::Firing) {
            const zeus::CQuaternion adjRot =
                zeus::CQuaternion::lookAt(armToTargetFlat.normalized(), lookDir, 2.f * M_PIF);
            armToTarget = adjRot.transform(armToTarget);
            if (x3bc_grappleSwingTimer >= 0.25f * g_tweakPlayer->GetGrappleSwingPeriod() &&
                x3bc_grappleSwingTimer < 0.75f * g_tweakPlayer->GetGrappleSwingPeriod()) {
              armToTarget = x490_gun->GetGrappleArm().GetTransform().basis[1];
            }
          }
        }
      }
      armXf = zeus::lookAt(zeus::skZero3f, armToTarget, zeus::skUp);
      armXf.origin = armPosition;
      x490_gun->GetGrappleArm().SetTransform(armXf);
    }
  }
}

float CPlayer::GetGravity() const {
  if (!g_GameState->GetPlayerState()->HasPowerUp(CPlayerState::EItemType::GravitySuit) && CheckSubmerged()) {
    return g_tweakPlayer->GetFluidGravAccel();
  }

  if (x37c_sidewaysDashing) {
    return -100.f;
  }

  return g_tweakPlayer->GetNormalGravAccel();
}

void CPlayer::ApplyGrappleForces(const CFinalInput& input, CStateManager& mgr, float dt) {
  if (const TCastToConstPtr<CScriptGrapplePoint> point = mgr.ObjectById(x310_orbitTargetId)) {
    switch (x3b8_grappleState) {
    case EGrappleState::Pull: {
      const zeus::CVector3f playerToPoint = point->GetTranslation() - GetTranslation();
      if (playerToPoint.canBeNormalized()) {
        zeus::CVector3f playerToSwingLow = point->GetTranslation() +
                                           zeus::CVector3f(0.f, 0.f, -g_tweakPlayer->GetGrappleSwingLength()) -
                                           GetTranslation();
        if (playerToSwingLow.canBeNormalized()) {
          const float distToSwingLow = playerToSwingLow.magnitude();
          playerToSwingLow.normalize();
          const float timeToLow = zeus::clamp(-1.f, distToSwingLow / g_tweakPlayer->GetGrapplePullSpeedProportion(), 1.f);
          const float pullSpeed =
              timeToLow * (g_tweakPlayer->GetGrapplePullSpeedMax() - g_tweakPlayer->GetGrapplePullSpeedMin()) +
              g_tweakPlayer->GetGrapplePullSpeedMin();
          SetVelocityWR(playerToSwingLow * pullSpeed);

          if (distToSwingLow < g_tweakPlayer->GetMaxGrappleLockedTurnAlignDistance()) {
            x3b8_grappleState = EGrappleState::Swinging;
            x3bc_grappleSwingTimer = 0.25f * g_tweakPlayer->GetGrappleSwingPeriod();
            x3d8_grappleJumpTimeout = 0.f;
            x9c6_28_aligningGrappleSwingTurn = point->GetGrappleParameters().GetLockSwingTurn();
          } else {
            const CMotionState mState = PredictMotion(dt);
            zeus::CVector3f lookDirFlat = x34_transform.basis[1];
            lookDirFlat.z() = 0.f;
            zeus::CVector3f newPlayerToPointFlat = point->GetTranslation() - (GetTranslation() + mState.x0_translation);
            newPlayerToPointFlat.z() = 0.f;
            if (lookDirFlat.canBeNormalized()) {
              lookDirFlat.normalize();
            }
            if (newPlayerToPointFlat.canBeNormalized()) {
              newPlayerToPointFlat.normalize();
            }
            const float lookToPointAngle = std::acos(zeus::clamp(-1.f, lookDirFlat.dot(newPlayerToPointFlat), 1.f));
            if (lookToPointAngle > 0.001f) {
              float deltaAngle = dt * g_tweakPlayer->GetGrappleLookCenterSpeed();
              if (lookToPointAngle >= deltaAngle) {
                zeus::CVector3f leftDirFlat(lookDirFlat.y(), -lookDirFlat.x(), 0.f);
                if (leftDirFlat.canBeNormalized()) {
                  leftDirFlat.normalize();
                }
                if (newPlayerToPointFlat.dot(leftDirFlat) >= 0.f) {
                  deltaAngle = -deltaAngle;
                }
                RotateToOR(zeus::CQuaternion::fromAxisAngle(zeus::skUp, deltaAngle), dt);
              } else if (std::fabs(lookToPointAngle - M_PIF) > 0.001f) {
                RotateToOR(zeus::CQuaternion::shortestRotationArc(lookDirFlat, newPlayerToPointFlat), dt);
              }
            } else {
              SetAngularVelocityWR(zeus::CAxisAngle());
              x174_torque = zeus::CAxisAngle();
            }
          }
        } else {
          x3b8_grappleState = EGrappleState::Swinging;
          x3bc_grappleSwingTimer = 0.25f * g_tweakPlayer->GetGrappleSwingPeriod();
          x3d8_grappleJumpTimeout = 0.f;
        }
      }
      break;
    }
    case EGrappleState::Swinging: {
      float turnAngleSpeed = zeus::degToRad(g_tweakPlayer->GetMaxGrappleTurnSpeed());
      if (g_tweakPlayer->GetInvertGrappleTurn()) {
        turnAngleSpeed *= -1.f;
      }
      const zeus::CVector3f pointToPlayer = GetTranslation() - point->GetTranslation();
      const float pointToPlayerZProj = zeus::clamp(-1.f, std::fabs(pointToPlayer.z() / pointToPlayer.magnitude()), 1.f);

      bool enableTurn = false;
      if (!point->GetGrappleParameters().GetLockSwingTurn()) {
        if (ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnLeft, input) > 0.05f) {
          enableTurn = true;
          turnAngleSpeed *= -ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnLeft, input);
        }
        if (ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnRight, input) > 0.05f) {
          enableTurn = true;
          turnAngleSpeed *= ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnRight, input);
        }
      } else if (x9c6_28_aligningGrappleSwingTurn) {
        enableTurn = true;
      }

      x3bc_grappleSwingTimer += dt;
      if (x3bc_grappleSwingTimer > g_tweakPlayer->GetGrappleSwingPeriod()) {
        x3bc_grappleSwingTimer -= g_tweakPlayer->GetGrappleSwingPeriod();
      }

      zeus::CVector3f swingAxis = x3c0_grappleSwingAxis;
      if (x3bc_grappleSwingTimer < 0.5f * g_tweakPlayer->GetGrappleSwingPeriod()) {
        swingAxis *= zeus::skNegOne3f;
      }

      const float pullSpeed =
          std::fabs(zeus::clamp(
              -1.f,
              std::cos(2.f * M_PIF * (x3bc_grappleSwingTimer / g_tweakPlayer->GetGrappleSwingPeriod()) + (M_PIF / 2.f)),
              1.f)) *
          g_tweakPlayer->GetGrapplePullSpeedMin();
      zeus::CVector3f pullVec = pointToPlayer.normalized().cross(swingAxis) * pullSpeed;
      pullVec += pointToPlayer *
                 zeus::clamp(-1.f,
                             (pointToPlayer.magnitude() - g_tweakPlayer->GetGrappleSwingLength()) /
                                 g_tweakPlayer->GetGrappleSwingLength(),
                             1.f) *
                 -32.f * pointToPlayerZProj;
      const zeus::CVector3f backupVel = x138_velocity;
      SetVelocityWR(pullVec);

      const zeus::CTransform backupXf = x34_transform;
      const CMotionState predMotion = PredictMotion(dt);
      const zeus::CVector3f newPos = x34_transform.origin + predMotion.x0_translation;
      if (ValidateFPPosition(newPos, mgr)) {
        if (enableTurn) {
          zeus::CQuaternion turnRot;
          turnRot.rotateZ(turnAngleSpeed * dt);
          if (point->GetGrappleParameters().GetLockSwingTurn() && x9c6_28_aligningGrappleSwingTurn) {
            zeus::CVector3f pointDir = point->GetTransform().basis[1].normalized();
            const zeus::CVector3f playerDir = x34_transform.basis[1].normalized();
            float playerPointProj = zeus::clamp(-1.f, playerDir.dot(pointDir), 1.f);
            if (std::fabs(playerPointProj) == 1.f) {
              x9c6_28_aligningGrappleSwingTurn = false;
            }
            if (playerPointProj < 0.f) {
              playerPointProj = -playerPointProj;
              pointDir = -pointDir;
            }

            const float turnAngleAdj = std::acos(playerPointProj) * dt;
            turnRot = zeus::CQuaternion::lookAt(playerDir, pointDir, turnAngleAdj);
          }

          if (pointToPlayer.magSquared() > 0.04f) {
            zeus::CVector3f pointToPlayerFlat = pointToPlayer;
            pointToPlayerFlat.z() = 0.f;
            zeus::CVector3f pointAtPlayerHeight = point->GetTranslation();
            pointAtPlayerHeight.z() = GetTranslation().z();
            const zeus::CVector3f playerToGrapplePlane =
                pointAtPlayerHeight + turnRot.transform(pointToPlayerFlat) - GetTranslation();
            if (playerToGrapplePlane.canBeNormalized()) {
              pullVec += playerToGrapplePlane / dt;
            }
          }

          const zeus::CVector3f swingAxisBackup = x3c0_grappleSwingAxis;
          x3c0_grappleSwingAxis = turnRot.transform(x3c0_grappleSwingAxis);
          x3c0_grappleSwingAxis.normalize();
          const zeus::CVector3f swingForward(-x3c0_grappleSwingAxis.y(), x3c0_grappleSwingAxis.x(), 0.f);
          SetTransform(zeus::CTransform(x3c0_grappleSwingAxis, swingForward, zeus::skUp, GetTranslation()));
          SetVelocityWR(pullVec);

          if (!ValidateFPPosition(GetTranslation(), mgr)) {
            x3c0_grappleSwingAxis = swingAxisBackup;
            SetTransform(backupXf);
            SetVelocityWR(backupVel);
          }
        }
      } else {
        BreakGrapple(EPlayerOrbitRequest::InvalidateTarget, mgr);
      }
      break;
    }
    case EGrappleState::JumpOff: {
      const zeus::CVector3f gravForce = {0.f, 0.f, GetGravity() * xe8_mass};
      ApplyForceOR(gravForce, zeus::CAxisAngle());
      break;
    }
    default:
      break;
    }
  }

  const zeus::CVector3f newAngVel = {0.f, 0.f, 0.9f * GetAngularVelocityOR().getVector().z()};
  SetAngularVelocityOR(newAngVel);
}

bool CPlayer::ValidateFPPosition(const zeus::CVector3f& pos, const CStateManager& mgr) const {
  constexpr CMaterialFilter solidFilter = CMaterialFilter::MakeInclude({EMaterialTypes::Solid});
  const zeus::CAABox aabb(x2d8_fpBounds.min - 1.f + pos, x2d8_fpBounds.max + 1.f + pos);
  rstl::reserved_vector<TUniqueId, 1024> nearList;
  mgr.BuildColliderList(nearList, *this, aabb);
  const CCollidableAABox colAABB({GetBaseBoundingBox().min + pos, GetBaseBoundingBox().max + pos}, {});
  return !CGameCollision::DetectCollisionBoolean(mgr, colAABB, zeus::CTransform(), solidFilter, nearList);
}

void CPlayer::UpdateGrappleState(const CFinalInput& input, CStateManager& mgr) {
  if (!mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::GrappleBeam) ||
      x2f8_morphBallState == EPlayerMorphBallState::Morphed ||
      mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan ||
      mgr.GetPlayerState()->GetTransitioningVisor() == CPlayerState::EPlayerVisor::Scan) {
    return;
  }

  if (x310_orbitTargetId == kInvalidUniqueId) {
    x3b8_grappleState = EGrappleState::None;
    AddMaterial(EMaterialTypes::GroundCollider, mgr);
    return;
  }

  const TCastToConstPtr<CScriptGrapplePoint> point = mgr.ObjectById(x310_orbitTargetId);
  if (point) {
    const zeus::CVector3f eyePosition = GetEyePosition();
    zeus::CVector3f playerToPoint = point->GetTranslation() - eyePosition;
    zeus::CVector3f playerToPointFlat = playerToPoint;
    playerToPointFlat.z() = 0.f;
    if (playerToPoint.canBeNormalized() && playerToPointFlat.canBeNormalized() && playerToPointFlat.magnitude() > 2.f) {
      switch (x304_orbitState) {
      case EPlayerOrbitState::Grapple:
        switch (g_tweakPlayer->GetGrappleJumpMode()) {
        case 0:
        case 1:
          if (ControlMapper::GetPressInput(ControlMapper::ECommands::FireOrBomb, input)) {
            if (const TCastToConstPtr<CScriptGrapplePoint> point2 = mgr.ObjectById(x33c_orbitNextTargetId)) {
              playerToPoint = point2->GetTranslation() - eyePosition;
              playerToPoint.z() = 0.f;
              if (playerToPoint.canBeNormalized()) {
                x490_gun->GetGrappleArm().GrappleBeamDisconnected();
                x3c0_grappleSwingAxis.x() = float(playerToPoint.y());
                x3c0_grappleSwingAxis.y() = -playerToPoint.x();
                x3c0_grappleSwingAxis.normalize();
                x3bc_grappleSwingTimer = 0.f;
                SetOrbitTargetId(x33c_orbitNextTargetId, mgr);
                x3b8_grappleState = EGrappleState::Pull;
                x33c_orbitNextTargetId = kInvalidUniqueId;
                x490_gun->GetGrappleArm().GrappleBeamConnected();
              }
            } else {
              if (g_tweakPlayer->GetGrappleJumpMode() == 0 && x3d8_grappleJumpTimeout <= 0.f) {
                ApplyGrappleJump(mgr);
              }
              BreakGrapple(EPlayerOrbitRequest::StopOrbit, mgr);
            }
          }
          break;
        default:
          break;
        }

        break;
      case EPlayerOrbitState::OrbitObject:
        if (playerToPoint.canBeNormalized()) {
          const CRayCastResult result = mgr.RayStaticIntersection(eyePosition, playerToPoint.normalized(),
                                                                  playerToPoint.magnitude(), LineOfSightFilter);
          if (result.IsInvalid()) {
            HolsterGun(mgr);
            switch (x3b8_grappleState) {
            case EGrappleState::Firing:
            case EGrappleState::Swinging:
              switch (g_tweakPlayer->GetGrappleJumpMode()) {
              case 0:
                switch (x490_gun->GetGrappleArm().GetAnimState()) {
                case CGrappleArm::EArmState::IntoGrappleIdle:
                  if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input)) {
                    x490_gun->GetGrappleArm().SetAnimState(CGrappleArm::EArmState::FireGrapple);
                  }
                  break;
                case CGrappleArm::EArmState::Connected:
                  BeginGrapple(playerToPoint, mgr);
                  break;
                default:
                  break;
                }
                break;
              case 1:
                if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input)) {
                  switch (x490_gun->GetGrappleArm().GetAnimState()) {
                  case CGrappleArm::EArmState::IntoGrappleIdle:
                    x490_gun->GetGrappleArm().SetAnimState(CGrappleArm::EArmState::FireGrapple);
                    break;
                  case CGrappleArm::EArmState::Connected:
                    BeginGrapple(playerToPoint, mgr);
                    break;
                  default:
                    break;
                  }
                  break;
                }
                break;
              case 2:
                switch (x490_gun->GetGrappleArm().GetAnimState()) {
                case CGrappleArm::EArmState::IntoGrappleIdle:
                  x490_gun->GetGrappleArm().SetAnimState(CGrappleArm::EArmState::FireGrapple);
                  break;
                case CGrappleArm::EArmState::Connected:
                  BeginGrapple(playerToPoint, mgr);
                  break;
                default:
                  break;
                }
                break;
              default:
                break;
              }
              break;
            case EGrappleState::None:
              x3b8_grappleState = EGrappleState::Firing;
              x490_gun->GetGrappleArm().Activate(true);
              break;
            default:
              break;
            }
          }
        }
        break;
      default:
        break;
      }
    }
  }

  if (x304_orbitState != EPlayerOrbitState::Grapple) {
    if (x304_orbitState >= EPlayerOrbitState::Grapple) {
      return;
    }
    if (x304_orbitState != EPlayerOrbitState::OrbitObject) {
      return;
    }
  } else {
    if (!point) {
      BreakGrapple(EPlayerOrbitRequest::Default, mgr);
      return;
    }

    switch (g_tweakPlayer->GetGrappleJumpMode()) {
    case 0:
      if (x3b8_grappleState == EGrappleState::JumpOff) {
        x3d8_grappleJumpTimeout -= input.DeltaTime();
        if (x3d8_grappleJumpTimeout <= 0.f) {
          BreakGrapple(EPlayerOrbitRequest::StopOrbit, mgr);
          SetMoveState(EPlayerMovementState::ApplyJump, mgr);
          ComputeMovement(input, mgr, input.DeltaTime());
          PreventFallingCameraPitch();
        }
      }
      break;
    case 1:
      switch (x3b8_grappleState) {
      case EGrappleState::Swinging:
        if (!ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input) &&
            x3d8_grappleJumpTimeout <= 0.f) {
          x3d8_grappleJumpTimeout = g_tweakPlayer->GetGrappleReleaseTime();
          x3b8_grappleState = EGrappleState::JumpOff;
          ApplyGrappleJump(mgr);
        }
        break;
      case EGrappleState::JumpOff:
        x3d8_grappleJumpTimeout -= input.DeltaTime();
        if (x3d8_grappleJumpTimeout <= 0.f) {
          SetMoveState(EPlayerMovementState::ApplyJump, mgr);
          ComputeMovement(input, mgr, input.DeltaTime());
          BreakGrapple(EPlayerOrbitRequest::StopOrbit, mgr);
          PreventFallingCameraPitch();
        }
        break;
      case EGrappleState::Firing:
      case EGrappleState::Pull:
        if (!ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input)) {
          BreakGrapple(EPlayerOrbitRequest::StopOrbit, mgr);
        }
        break;
      default:
        break;
      }
      break;
    default:
      break;
    }

    const zeus::CVector3f eyePos = GetEyePosition();
    const zeus::CVector3f playerToPoint = point->GetTranslation() - eyePos;
    if (playerToPoint.canBeNormalized()) {
      const CRayCastResult result =
          mgr.RayStaticIntersection(eyePos, playerToPoint.normalized(), playerToPoint.magnitude(), LineOfSightFilter);
      if (result.IsValid()) {
        BreakGrapple(EPlayerOrbitRequest::LostGrappleLineOfSight, mgr);
      }
    }
    return;
  }

  if (x490_gun->GetGrappleArm().BeamActive() && g_tweakPlayer->GetGrappleJumpMode() == 1 &&
      !ControlMapper::GetDigitalInput(ControlMapper::ECommands::FireOrBomb, input)) {
    BreakGrapple(EPlayerOrbitRequest::StopOrbit, mgr);
  }
}

void CPlayer::ApplyGrappleJump(CStateManager& mgr) {
  const TCastToConstPtr<CScriptGrapplePoint> point = mgr.ObjectById(x310_orbitTargetId);

  if (!point) {
    return;
  }

  zeus::CVector3f tmp = x3c0_grappleSwingAxis;
  if (x3bc_grappleSwingTimer < 0.5f * g_tweakPlayer->GetGrappleSwingPeriod()) {
    tmp *= zeus::skNegOne3f;
  }
  const zeus::CVector3f pointToPlayer = GetTranslation() - point->GetTranslation();
  const zeus::CVector3f cross = pointToPlayer.normalized().cross(tmp);
  const zeus::CVector3f pointToPlayerFlat(pointToPlayer.x(), pointToPlayer.y(), 0.f);
  float dot = 1.f;
  if (pointToPlayerFlat.canBeNormalized() && cross.canBeNormalized()) {
    dot = zeus::clamp(-1.f, std::fabs(cross.normalized().dot(pointToPlayerFlat.normalized())), 1.f);
  }
  ApplyForceWR(g_tweakPlayer->GetGrappleJumpForce() * cross * 10000.f * dot, zeus::CAxisAngle());
}

void CPlayer::BeginGrapple(zeus::CVector3f& vec, CStateManager& mgr) {
  vec.z() = 0.f;
  if (vec.canBeNormalized()) {
    x3c0_grappleSwingAxis.x() = float(vec.y());
    x3c0_grappleSwingAxis.y() = -vec.x();
    x3c0_grappleSwingAxis.normalize();
    x3bc_grappleSwingTimer = 0.f;
    SetOrbitState(EPlayerOrbitState::Grapple, mgr);
    x3b8_grappleState = EGrappleState::Pull;
    RemoveMaterial(EMaterialTypes::GroundCollider, mgr);
  }
}

void CPlayer::BreakGrapple(EPlayerOrbitRequest req, CStateManager& mgr) {
  x294_jumpCameraTimer = 0.f;
  x29c_fallCameraTimer = 0.f;
  if (g_tweakPlayer->GetGrappleJumpMode() == 2 && x3b8_grappleState == EGrappleState::Swinging) {
    ApplyGrappleJump(mgr);
    PreventFallingCameraPitch();
  }

  SetOrbitRequest(req, mgr);
  x3b8_grappleState = EGrappleState::None;
  AddMaterial(EMaterialTypes::GroundCollider, mgr);
  x490_gun->GetGrappleArm().SetAnimState(CGrappleArm::EArmState::OutOfGrapple);
  if (!InGrappleJumpCooldown() && x3b8_grappleState != EGrappleState::JumpOff) {
    DrawGun(mgr);
  }
}

void CPlayer::SetOrbitRequest(EPlayerOrbitRequest req, CStateManager& mgr) {
  x30c_orbitRequest = req;
  switch (req) {
  case EPlayerOrbitRequest::ActivateOrbitSource:
    ActivateOrbitSource(mgr);
    break;
  case EPlayerOrbitRequest::BadVerticalAngle:
    SetOrbitState(EPlayerOrbitState::OrbitPoint, mgr);
    x314_orbitPoint =
        g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)) * x34_transform.basis[1] + x34_transform.origin;
    break;
  default:
    SetOrbitState(EPlayerOrbitState::NoOrbit, mgr);
    break;
  }
}

void CPlayer::SetOrbitRequestForTarget(TUniqueId id, EPlayerOrbitRequest req, CStateManager& mgr) {
  switch (x304_orbitState) {
  case EPlayerOrbitState::OrbitObject:
  case EPlayerOrbitState::ForcedOrbitObject:
  case EPlayerOrbitState::Grapple:
    if (id == x310_orbitTargetId)
      SetOrbitRequest(req, mgr);
    break;
  default:
    break;
  }
}

bool CPlayer::InGrappleJumpCooldown() const {
  if (x258_movementState == EPlayerMovementState::OnGround) {
    return false;
  }
  return x3d8_grappleJumpTimeout > 0.f || x294_jumpCameraTimer == 0.f;
}

void CPlayer::PreventFallingCameraPitch() {
  x294_jumpCameraTimer = 0.f;
  x29c_fallCameraTimer = 0.01f;
  x2a4_cancelCameraPitch = true;
}

void CPlayer::OrbitCarcass(CStateManager& mgr) {
  if (x304_orbitState != EPlayerOrbitState::OrbitObject) {
    return;
  }

  x308_orbitType = EPlayerOrbitType::Default;
  SetOrbitState(EPlayerOrbitState::OrbitCarcass, mgr);
}

void CPlayer::OrbitPoint(EPlayerOrbitType type, CStateManager& mgr) {
  x308_orbitType = type;
  SetOrbitState(EPlayerOrbitState::OrbitPoint, mgr);
  SetOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
}

zeus::CVector3f CPlayer::GetHUDOrbitTargetPosition() const {
  return x314_orbitPoint + x76c_cameraBob->GetCameraBobTransformation().origin;
}

void CPlayer::SetOrbitState(EPlayerOrbitState state, CStateManager& mgr) {
  x304_orbitState = state;
  CFirstPersonCamera* cam = mgr.GetCameraManager()->GetFirstPersonCamera();
  switch (x304_orbitState) {
  case EPlayerOrbitState::OrbitObject:
#ifndef NDEBUG
    if (x310_orbitTargetId != kInvalidUniqueId) {
      if (const CEntity* ent = mgr.GetObjectById(x310_orbitTargetId)) {
        Log.report(logvisor::Info, FMT_STRING("Orbiting {} {}"), ent->GetEditorId(), ent->GetName());
      }
    }
#endif
    cam->SetLockCamera(false);
    break;
  case EPlayerOrbitState::OrbitCarcass: {
    cam->SetLockCamera(true);
    const zeus::CVector3f playerToPoint = x314_orbitPoint - GetTranslation();
    if (playerToPoint.canBeNormalized()) {
      x340_ = playerToPoint.magnitude();
    } else {
      x340_ = 0.f;
    }
    SetOrbitTargetId(kInvalidUniqueId, mgr);
    x33c_orbitNextTargetId = kInvalidUniqueId;
    break;
  }
  case EPlayerOrbitState::NoOrbit:
    x32c_orbitModeTimer = g_tweakPlayer->GetOrbitModeTimer();
    x32c_orbitModeTimer = 0.28f;
    SetOrbitTargetId(kInvalidUniqueId, mgr);
    x33c_orbitNextTargetId = kInvalidUniqueId;
    break;
  case EPlayerOrbitState::OrbitPoint:
    SetOrbitTargetId(kInvalidUniqueId, mgr);
    x33c_orbitNextTargetId = kInvalidUniqueId;
    break;
  default:
    break;
  }
}

void CPlayer::SetOrbitTargetId(TUniqueId id, CStateManager& mgr) {
  if (id != kInvalidUniqueId) {
    x394_orbitingEnemy =
        (TCastToPtr<CPatterned>(mgr.ObjectById(id)) || TCastToPtr<CWallCrawlerSwarm>(mgr.ObjectById(id)) ||
         CPatterned::CastTo<MP1::CThardusRockProjectile>(mgr.ObjectById(id)) ||
         TCastToPtr<CScriptGunTurret>(mgr.ObjectById(id)));
  }

  x310_orbitTargetId = id;
  if (x310_orbitTargetId == kInvalidUniqueId) {
    x374_orbitLockEstablished = false;
  }
}

void CPlayer::UpdateOrbitPosition(float dist, CStateManager& mgr) {
  switch (x304_orbitState) {
  case EPlayerOrbitState::OrbitPoint:
  case EPlayerOrbitState::OrbitCarcass:
    SetOrbitPosition(dist, mgr);
    break;
  case EPlayerOrbitState::OrbitObject:
  case EPlayerOrbitState::ForcedOrbitObject:
  case EPlayerOrbitState::Grapple:
    if (const TCastToConstPtr<CActor> act = mgr.ObjectById(x310_orbitTargetId)) {
      if (x310_orbitTargetId != kInvalidUniqueId) {
        x314_orbitPoint = act->GetOrbitPosition(mgr);
      }
    }
    break;
  default:
    break;
  }
}

void CPlayer::UpdateOrbitZPosition() {
  if (x304_orbitState != EPlayerOrbitState::OrbitPoint) {
    return;
  }

  if (std::fabs(x320_orbitVector.z()) >= g_tweakPlayer->GetOrbitZRange()) {
    return;
  }

  x314_orbitPoint.z() = x320_orbitVector.z() + x34_transform.origin.z() + GetEyeHeight();
}

void CPlayer::UpdateOrbitFixedPosition() {
  x314_orbitPoint = x34_transform.rotate(x320_orbitVector) + GetEyePosition();
}

void CPlayer::SetOrbitPosition(float dist, CStateManager& mgr) {
  zeus::CTransform camXf = GetFirstPersonCameraTransform(mgr);
  if (x304_orbitState == EPlayerOrbitState::OrbitPoint && x30c_orbitRequest == EPlayerOrbitRequest::BadVerticalAngle) {
    camXf = x34_transform;
  }

  const zeus::CVector3f fwd = camXf.basis[1];
  float dot = fwd.normalized().dot(fwd);
  if (std::fabs(dot) > 1.f) {
    dot = (dot > 0.f) ? 1.f : -1.f;
  }

  x314_orbitPoint = camXf.rotate(zeus::CVector3f(0.f, dist / dot, 0.f)) + camXf.origin;
  x320_orbitVector = zeus::CVector3f(0.f, dist, x314_orbitPoint.z() - camXf.origin.z());
}

void CPlayer::UpdateAimTarget(CStateManager& mgr) {
  if (!ValidateAimTargetId(x3f4_aimTarget, mgr)) {
    ResetAimTargetPrediction(kInvalidUniqueId);
  }

  if (!GetCombatMode()) {
    ResetAimTargetPrediction(kInvalidUniqueId);
    x48c_aimTargetTimer = 0.f;
    return;
  }

#if 0
    if (!0 && 0)
    {
        ResetAimTargetPrediction(kInvalidUniqueId);
        x48c_aimTargetTimer = 0.f;
        if (x304_orbitState == EPlayerOrbitState::One ||
            x304_orbitState == EPlayerOrbitState::Four) {
            if (!ValidateOrbitTargetId(x310_orbitTargetId, mgr)) {
                ResetAimTargetPrediction(x310_orbitTargetId);
            }
        }
        return;
    }
#endif

  bool needsReset = false;
  const TCastToConstPtr<CActor> act = mgr.ObjectById(x3f4_aimTarget);
  const CActor* actp = act.GetPtr();
  if (act) {
    if (!act->GetMaterialList().HasMaterial(EMaterialTypes::Target)) {
      actp = nullptr;
    }
  }

  if (g_tweakPlayer->GetAimWhenOrbitingPoint()) {
    if (x304_orbitState == EPlayerOrbitState::OrbitObject || x304_orbitState == EPlayerOrbitState::ForcedOrbitObject) {
      if (ValidateOrbitTargetId(x310_orbitTargetId, mgr) == EOrbitValidationResult::OK) {
        ResetAimTargetPrediction(x310_orbitTargetId);
      } else {
        needsReset = true;
      }
    } else {
      needsReset = true;
    }
  } else {
    if (x304_orbitState == EPlayerOrbitState::NoOrbit) {
      needsReset = true;
    }
  }

  if (!needsReset) {
    return;
  }

  if (actp && ValidateObjectForMode(x3f4_aimTarget, mgr)) {
    ResetAimTargetPrediction(kInvalidUniqueId);
  } else {
    ResetAimTargetPrediction(FindAimTargetId(mgr));
  }
}

void CPlayer::UpdateAimTargetTimer(float dt) {
  if (x3f4_aimTarget == kInvalidUniqueId) {
    return;
  }

  if (x48c_aimTargetTimer <= 0.f) {
    return;
  }

  x48c_aimTargetTimer -= dt;
}

bool CPlayer::ValidateAimTargetId(TUniqueId uid, CStateManager& mgr) {
  if (uid == kInvalidUniqueId) {
    x404_aimTargetAverage.Clear();
    x48c_aimTargetTimer = 0.f;
    return false;
  }

  const TCastToConstPtr<CActor> act = mgr.ObjectById(uid);
  if (!act || !act->GetMaterialList().HasMaterial(EMaterialTypes::Target) || !act->GetIsTargetable()) {
    return false;
  }

  if (x304_orbitState == EPlayerOrbitState::OrbitObject || x304_orbitState == EPlayerOrbitState::ForcedOrbitObject) {
    if (ValidateOrbitTargetId(x310_orbitTargetId, mgr) != EOrbitValidationResult::OK) {
      ResetAimTargetPrediction(kInvalidUniqueId);
      x48c_aimTargetTimer = 0.f;
      return false;
    }
    return true;
  }

  if (act->GetMaterialList().HasMaterial(EMaterialTypes::Target) && uid != kInvalidUniqueId &&
      ValidateObjectForMode(uid, mgr)) {
    const float vpWHalf = g_Viewport.x8_width / 2;
    const float vpHHalf = g_Viewport.xc_height / 2;
    const zeus::CVector3f aimPos = act->GetAimPosition(mgr, 0.f);
    const zeus::CVector3f eyePos = GetEyePosition();
    zeus::CVector3f eyeToAim = aimPos - eyePos;
    const zeus::CVector3f screenPos = mgr.GetCameraManager()->GetFirstPersonCamera()->ConvertToScreenSpace(aimPos);
    const zeus::CVector3f posInBox(vpWHalf + screenPos.x() * vpWHalf, vpHHalf + screenPos.y() * vpHHalf, screenPos.z());
    if (WithinOrbitScreenBox(posInBox, x330_orbitZoneMode, x334_orbitType) ||
        (x330_orbitZoneMode != EPlayerZoneInfo::Targeting &&
         WithinOrbitScreenBox(posInBox, EPlayerZoneInfo::Targeting, x334_orbitType))) {
      const float eyeToAimMag = eyeToAim.magnitude();
      if (eyeToAimMag <= g_tweakPlayer->GetAimMaxDistance()) {
        rstl::reserved_vector<TUniqueId, 1024> nearList;
        TUniqueId intersectId = kInvalidUniqueId;
        eyeToAim.normalize();
        mgr.BuildNearList(nearList, eyePos, eyeToAim, eyeToAimMag, OccluderFilter, act);
        eyeToAim.normalize();
        const CRayCastResult result =
            mgr.RayWorldIntersection(intersectId, eyePos, eyeToAim, eyeToAimMag, LineOfSightFilter, nearList);
        if (result.IsInvalid()) {
          x48c_aimTargetTimer = g_tweakPlayer->GetAimTargetTimer();
          return true;
        }
      }
    }

    if (x48c_aimTargetTimer > 0.f) {
      return true;
    }
  }

  ResetAimTargetPrediction(kInvalidUniqueId);
  x48c_aimTargetTimer = 0.f;
  return false;
}

bool CPlayer::ValidateObjectForMode(TUniqueId uid, CStateManager& mgr) const {
  const TCastToPtr<CActor> act = mgr.ObjectById(uid);
  if (!act || uid == kInvalidUniqueId) {
    return false;
  }

  if (TCastToConstPtr<CScriptDoor>(mgr.ObjectById(uid))) {
    return true;
  }

  if (GetCombatMode()) {
    if (const CHealthInfo* hInfo = act->HealthInfo(mgr)) {
      if (hInfo->GetHP() > 0.f) {
        return true;
      }
    } else {
      if (act->GetMaterialList().HasMaterial(EMaterialTypes::Projectile) ||
          act->GetMaterialList().HasMaterial(EMaterialTypes::Scannable)) {
        return true;
      }

      if (const TCastToConstPtr<CScriptGrapplePoint> point = mgr.ObjectById(uid)) {
        const zeus::CVector3f playerToPoint = point->GetTranslation() - GetTranslation();
        if (playerToPoint.canBeNormalized() && playerToPoint.magnitude() < g_tweakPlayer->GetOrbitDistanceMax()) {
          return true;
        }
      }
    }
  }

  if (GetExplorationMode()) {
    if (!act->HealthInfo(mgr)) {
      if (const TCastToConstPtr<CScriptGrapplePoint> point = mgr.ObjectById(uid)) {
        const zeus::CVector3f playerToPoint = point->GetTranslation() - GetTranslation();
        if (playerToPoint.canBeNormalized() && playerToPoint.magnitude() < g_tweakPlayer->GetOrbitDistanceMax()) {
          return true;
        }
      } else {
        return true;
      }
    } else {
      return true;
    }
  }

  return false;
}

static zeus::CAABox BuildNearListBox(bool cropBottom, const zeus::CTransform& xf, float x, float z, float y) {
  const zeus::CAABox aabb(-x, cropBottom ? 0.f : -y, -z, x, y, z);
  return aabb.getTransformedAABox(xf);
}

TUniqueId CPlayer::FindAimTargetId(CStateManager& mgr) const {
  float dist = g_tweakPlayer->GetAimMaxDistance();
  if (x9c6_24_extendTargetDistance) {
    dist *= 5.f;
  }
  const zeus::CAABox aabb = BuildNearListBox(true, GetFirstPersonCameraTransform(mgr), g_tweakPlayer->GetAimBoxWidth(),
                                             g_tweakPlayer->GetAimBoxHeight(), dist);
  rstl::reserved_vector<TUniqueId, 1024> nearList;
  mgr.BuildNearList(nearList, aabb, CMaterialFilter::MakeInclude({EMaterialTypes::Target}), this);
  return CheckEnemiesAgainstOrbitZone(nearList, EPlayerZoneInfo::Targeting, EPlayerZoneType::Ellipse, mgr);
}

const zeus::CTransform& CPlayer::GetFirstPersonCameraTransform(const CStateManager& mgr) const {
  return mgr.GetCameraManager()->GetFirstPersonCamera()->GetGunFollowTransform();
}

TUniqueId CPlayer::CheckEnemiesAgainstOrbitZone(const rstl::reserved_vector<TUniqueId, 1024>& list,
                                                EPlayerZoneInfo info, EPlayerZoneType zone, CStateManager& mgr) const {
  const zeus::CVector3f eyePos = GetEyePosition();
  float minEyeToAimMag = 10000.f;
  float minPosInBoxMagSq = 10000.f;
  TUniqueId bestId = kInvalidUniqueId;
  const float vpWHalf = g_Viewport.x8_width / 2;
  const float vpHHalf = g_Viewport.xc_height / 2;
  const float boxLeft = (GetOrbitZoneIdealXScaled(int(info)) - vpWHalf) / vpWHalf;
  const float boxTop = (GetOrbitZoneIdealYScaled(int(info)) - vpHHalf) / vpHHalf;
  const CFirstPersonCamera* fpCam = mgr.GetCameraManager()->GetFirstPersonCamera();

  for (const auto& id : list) {
    if (const auto* act = static_cast<CActor*>(mgr.ObjectById(id))) {
      if (act->GetUniqueId() != GetUniqueId() && ValidateObjectForMode(act->GetUniqueId(), mgr)) {
        const zeus::CVector3f aimPos = act->GetAimPosition(mgr, 0.f);
        const zeus::CVector3f screenPos = fpCam->ConvertToScreenSpace(aimPos);
        const zeus::CVector3f posInBox(vpWHalf + screenPos.x() * vpWHalf, vpHHalf + screenPos.y() * vpHHalf, screenPos.z());
        if (WithinOrbitScreenBox(posInBox, info, zone)) {
          zeus::CVector3f eyeToAim = aimPos - eyePos;
          const float eyeToAimMag = eyeToAim.magnitude();
          if (eyeToAimMag <= g_tweakPlayer->GetAimMaxDistance()) {
            if (minEyeToAimMag - eyeToAimMag > g_tweakPlayer->GetAimThresholdDistance()) {
              rstl::reserved_vector<TUniqueId, 1024> nearList;
              TUniqueId intersectId = kInvalidUniqueId;
              eyeToAim.normalize();
              mgr.BuildNearList(nearList, eyePos, eyeToAim, eyeToAimMag, OccluderFilter, act);
              eyeToAim.normalize();
              const CRayCastResult result =
                  mgr.RayWorldIntersection(intersectId, eyePos, eyeToAim, eyeToAimMag, LineOfSightFilter, nearList);
              if (result.IsInvalid()) {
                bestId = act->GetUniqueId();
                const float posInBoxLeft = posInBox.x() - boxLeft;
                const float posInBoxTop = posInBox.y() - boxTop;
                minEyeToAimMag = eyeToAimMag;
                minPosInBoxMagSq = posInBoxLeft * posInBoxLeft + posInBoxTop * posInBoxTop;
              }
            } else if (std::fabs(eyeToAimMag - minEyeToAimMag) < g_tweakPlayer->GetAimThresholdDistance()) {
              const float posInBoxLeft = posInBox.x() - boxLeft;
              const float posInBoxTop = posInBox.y() - boxTop;
              const float posInBoxMagSq = posInBoxLeft * posInBoxLeft + posInBoxTop * posInBoxTop;
              if (posInBoxMagSq < minPosInBoxMagSq) {
                rstl::reserved_vector<TUniqueId, 1024> nearList;
                TUniqueId intersectId = kInvalidUniqueId;
                eyeToAim.normalize();
                mgr.BuildNearList(nearList, eyePos, eyeToAim, eyeToAimMag, OccluderFilter, act);
                eyeToAim.normalize();
                const CRayCastResult result =
                    mgr.RayWorldIntersection(intersectId, eyePos, eyeToAim, eyeToAimMag, LineOfSightFilter, nearList);
                if (result.IsInvalid()) {
                  bestId = act->GetUniqueId();
                  minEyeToAimMag = eyeToAimMag;
                  minPosInBoxMagSq = posInBoxMagSq;
                }
              }
            }
          }
        }
      }
    }
  }

  return bestId;
}

TUniqueId CPlayer::FindOrbitTargetId(CStateManager& mgr) const {
  return FindBestOrbitableObject(x354_onScreenOrbitObjects, x330_orbitZoneMode, mgr);
}

void CPlayer::UpdateOrbitableObjects(CStateManager& mgr) {
  x354_onScreenOrbitObjects.clear();
  x344_nearbyOrbitObjects.clear();
  x364_offScreenOrbitObjects.clear();

  if (CheckOrbitDisableSourceList(mgr)) {
    return;
  }

  float dist = GetOrbitMaxTargetDistance(mgr);
  if (x9c6_24_extendTargetDistance) {
    dist *= 5.f;
  }
  const zeus::CAABox nearAABB = BuildNearListBox(true, GetFirstPersonCameraTransform(mgr),
                                                 g_tweakPlayer->GetOrbitNearX(), g_tweakPlayer->GetOrbitNearZ(), dist);

  const CMaterialFilter filter = mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan
                                     ? CMaterialFilter::MakeInclude({EMaterialTypes::Scannable})
                                     : CMaterialFilter::MakeInclude({EMaterialTypes::Orbit});
  rstl::reserved_vector<TUniqueId, 1024> nearList;
  mgr.BuildNearList(nearList, nearAABB, filter, nullptr);

  FindOrbitableObjects(nearList, x344_nearbyOrbitObjects, x330_orbitZoneMode, EPlayerZoneType::Always, mgr, true);
  FindOrbitableObjects(nearList, x354_onScreenOrbitObjects, x330_orbitZoneMode, x334_orbitType, mgr, true);
  FindOrbitableObjects(nearList, x364_offScreenOrbitObjects, x330_orbitZoneMode, x334_orbitType, mgr, false);
}

TUniqueId CPlayer::FindBestOrbitableObject(const std::vector<TUniqueId>& ids, EPlayerZoneInfo info,
                                           CStateManager& mgr) const {
  const zeus::CVector3f eyePos = GetEyePosition();
  float minEyeToOrbitMag = 10000.f;
  float minPosInBoxMagSq = 10000.f;
  TUniqueId bestId = kInvalidUniqueId;
  const float vpWidthHalf = g_Viewport.x8_width / 2;
  const float vpHeightHalf = g_Viewport.xc_height / 2;
  const float boxLeft = (GetOrbitZoneIdealXScaled(int(info)) - vpWidthHalf) / vpWidthHalf;
  const float boxTop = (GetOrbitZoneIdealYScaled(int(info)) - vpHeightHalf) / vpHeightHalf;

  const CFirstPersonCamera* fpCam = mgr.GetCameraManager()->GetFirstPersonCamera();

  for (const auto& id : ids) {
    if (const TCastToConstPtr<CActor> act = mgr.ObjectById(id)) {
      const zeus::CVector3f orbitPos = act->GetOrbitPosition(mgr);
      zeus::CVector3f eyeToOrbit = orbitPos - eyePos;
      const float eyeToOrbitMag = eyeToOrbit.magnitude();
      const zeus::CVector3f orbitPosScreen = fpCam->ConvertToScreenSpace(orbitPos);
      if (orbitPosScreen.z() >= 0.f) {
        if (x310_orbitTargetId != id) {
          if (const TCastToConstPtr<CScriptGrapplePoint> point = act.GetPtr()) {
            if (x310_orbitTargetId != point->GetUniqueId()) {
              if (mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::GrappleBeam) &&
                  eyeToOrbitMag < minEyeToOrbitMag && eyeToOrbitMag < g_tweakPlayer->GetOrbitDistanceMax()) {
                rstl::reserved_vector<TUniqueId, 1024> nearList;
                TUniqueId intersectId = kInvalidUniqueId;
                eyeToOrbit.normalize();
                mgr.BuildNearList(nearList, eyePos, eyeToOrbit, eyeToOrbitMag, OccluderFilter, act.GetPtr());
                eyeToOrbit.normalize();
                const CRayCastResult result = mgr.RayWorldIntersection(intersectId, eyePos, eyeToOrbit, eyeToOrbitMag,
                                                                       LineOfSightFilter, nearList);
                if (result.IsInvalid()) {
                  if (point->GetGrappleParameters().GetLockSwingTurn()) {
                    zeus::CVector3f pointToPlayer = GetTranslation() - point->GetTranslation();
                    if (pointToPlayer.canBeNormalized()) {
                      pointToPlayer.z() = 0.f;
                      if (std::fabs(point->GetTransform().basis[1].normalized().dot(pointToPlayer.normalized())) <=
                          M_SQRT1_2F) {
                        continue;
                      }
                    }
                  }

                  bestId = act->GetUniqueId();
                  const float posInBoxLeft = orbitPosScreen.x() - boxLeft;
                  const float posInBoxTop = orbitPosScreen.y() - boxTop;
                  minEyeToOrbitMag = eyeToOrbitMag;
                  minPosInBoxMagSq = posInBoxLeft * posInBoxLeft + posInBoxTop * posInBoxTop;
                }
              }
              continue;
            }
          }

          if (minEyeToOrbitMag - eyeToOrbitMag > g_tweakPlayer->GetOrbitDistanceThreshold() &&
              mgr.GetPlayerState()->GetCurrentVisor() != CPlayerState::EPlayerVisor::Scan) {
            rstl::reserved_vector<TUniqueId, 1024> nearList;
            TUniqueId idOut = kInvalidUniqueId;
            eyeToOrbit.normalize();
            mgr.BuildNearList(nearList, eyePos, eyeToOrbit, eyeToOrbitMag, OccluderFilter, act.GetPtr());
            for (auto it = nearList.begin(); it != nearList.end();) {
              if (const CEntity* obj = mgr.ObjectById(*it)) {
                if (obj->GetAreaIdAlways() != kInvalidAreaId) {
                  if (mgr.GetNextAreaId() != obj->GetAreaIdAlways()) {
                    const CGameArea* area = mgr.GetWorld()->GetAreaAlways(obj->GetAreaIdAlways());
                    const CGameArea::EOcclusionState state =
                        area->IsPostConstructed() ? area->GetOcclusionState() : CGameArea::EOcclusionState::Occluded;
                    if (state == CGameArea::EOcclusionState::Occluded) {
                      it = nearList.erase(it);
                      continue;
                    }
                  }
                } else {
                  it = nearList.erase(it);
                  continue;
                }
              }
              ++it;
            }

            eyeToOrbit.normalize();
            const CRayCastResult result =
                mgr.RayWorldIntersection(idOut, eyePos, eyeToOrbit, eyeToOrbitMag, LineOfSightFilter, nearList);
            if (result.IsInvalid()) {
              bestId = act->GetUniqueId();
              const float posInBoxLeft = orbitPosScreen.x() - boxLeft;
              const float posInBoxTop = orbitPosScreen.y() - boxTop;
              minEyeToOrbitMag = eyeToOrbitMag;
              minPosInBoxMagSq = posInBoxLeft * posInBoxLeft + posInBoxTop * posInBoxTop;
            }
            continue;
          }

          if (std::fabs(eyeToOrbitMag - minEyeToOrbitMag) < g_tweakPlayer->GetOrbitDistanceThreshold() ||
              mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan) {
            const float posInBoxLeft = orbitPosScreen.x() - boxLeft;
            const float posInBoxTop = orbitPosScreen.y() - boxTop;
            const float posInBoxMagSq = posInBoxLeft * posInBoxLeft + posInBoxTop * posInBoxTop;
            if (posInBoxMagSq < minPosInBoxMagSq) {
              rstl::reserved_vector<TUniqueId, 1024> nearList;
              TUniqueId idOut = kInvalidUniqueId;
              eyeToOrbit.normalize();
              mgr.BuildNearList(nearList, eyePos, eyeToOrbit, eyeToOrbitMag, OccluderFilter, act.GetPtr());
              for (auto it = nearList.begin(); it != nearList.end();) {
                if (const CEntity* obj = mgr.ObjectById(*it)) {
                  if (obj->GetAreaIdAlways() != kInvalidAreaId) {
                    if (mgr.GetNextAreaId() != obj->GetAreaIdAlways()) {
                      const CGameArea* area = mgr.GetWorld()->GetAreaAlways(obj->GetAreaIdAlways());
                      const CGameArea::EOcclusionState state =
                          area->IsPostConstructed() ? area->GetOcclusionState() : CGameArea::EOcclusionState::Occluded;
                      if (state == CGameArea::EOcclusionState::Occluded) {
                        it = nearList.erase(it);
                        continue;
                      }
                    }
                  } else {
                    it = nearList.erase(it);
                    continue;
                  }
                }
                ++it;
              }

              eyeToOrbit.normalize();
              const CRayCastResult result =
                  mgr.RayWorldIntersection(idOut, eyePos, eyeToOrbit, eyeToOrbitMag, LineOfSightFilter, nearList);
              if (result.IsInvalid()) {
                bestId = act->GetUniqueId();
                minPosInBoxMagSq = posInBoxMagSq;
                minEyeToOrbitMag = eyeToOrbitMag;
              }
            }
          }
        }
      }
    }
  }

  return bestId;
}

void CPlayer::FindOrbitableObjects(const rstl::reserved_vector<TUniqueId, 1024>& nearObjects,
                                   std::vector<TUniqueId>& listOut, EPlayerZoneInfo zone, EPlayerZoneType type,
                                   CStateManager& mgr, bool onScreenTest) const {
  const CFirstPersonCamera* fpCam = mgr.GetCameraManager()->GetFirstPersonCamera();
  const zeus::CVector3f eyePos = GetEyePosition();

  for (const auto& id : nearObjects) {
    if (const TCastToConstPtr<CActor> act = mgr.GetObjectById(id)) {
      if (GetUniqueId() == act->GetUniqueId()) {
        continue;
      }
      if (ValidateOrbitTargetId(act->GetUniqueId(), mgr) != EOrbitValidationResult::OK) {
        continue;
      }
      const zeus::CVector3f orbitPos = act->GetOrbitPosition(mgr);
      zeus::CVector3f screenPos = fpCam->ConvertToScreenSpace(orbitPos);
      screenPos.x() = g_Viewport.x8_width * screenPos.x() / 2.f + g_Viewport.x8_width / 2.f;
      screenPos.y() = g_Viewport.xc_height * screenPos.y() / 2.f + g_Viewport.xc_height / 2.f;

      bool pass = false;
      if (onScreenTest) {
        if (WithinOrbitScreenBox(screenPos, zone, type)) {
          pass = true;
        }
      } else {
        if (!WithinOrbitScreenBox(screenPos, zone, type)) {
          pass = true;
        }
      }

      if (pass &&
          (!act->GetDoTargetDistanceTest() || (orbitPos - eyePos).magnitude() <= GetOrbitMaxTargetDistance(mgr))) {
        listOut.push_back(id);
      }
    }
  }
}

bool CPlayer::WithinOrbitScreenBox(const zeus::CVector3f& screenCoords, EPlayerZoneInfo zone,
                                   EPlayerZoneType type) const {
  if (screenCoords.z() >= 1.f) {
    return false;
  }

  switch (type) {
  case EPlayerZoneType::Box:
    return std::fabs(screenCoords.x() - GetOrbitScreenBoxCenterXScaled(int(zone))) <
               GetOrbitScreenBoxHalfExtentXScaled(int(zone)) &&
           std::fabs(screenCoords.y() - GetOrbitScreenBoxCenterYScaled(int(zone))) <
               GetOrbitScreenBoxHalfExtentYScaled(int(zone)) &&
           screenCoords.z() < 1.f;
  case EPlayerZoneType::Ellipse:
    return WithinOrbitScreenEllipse(screenCoords, zone);
  default:
    return true;
  }
}

bool CPlayer::WithinOrbitScreenEllipse(const zeus::CVector3f& screenCoords, EPlayerZoneInfo zone) const {
  if (screenCoords.z() >= 1.f) {
    return false;
  }

  float heYSq = GetOrbitScreenBoxHalfExtentYScaled(int(zone));
  heYSq *= heYSq;
  float heXSq = GetOrbitScreenBoxHalfExtentXScaled(int(zone));
  heXSq *= heXSq;
  const float tmpY = std::fabs(screenCoords.y() - GetOrbitScreenBoxCenterYScaled(int(zone)));
  const float tmpX = std::fabs(screenCoords.x() - GetOrbitScreenBoxCenterXScaled(int(zone)));
  return tmpX * tmpX <= (1.f - tmpY * tmpY / heYSq) * heXSq;
}

bool CPlayer::CheckOrbitDisableSourceList(CStateManager& mgr) {
  for (auto it = x9e4_orbitDisableList.begin(); it != x9e4_orbitDisableList.end();) {
    if (mgr.GetObjectById(*it) == nullptr) {
      it = x9e4_orbitDisableList.erase(it);
      continue;
    }
    ++it;
  }
  return !x9e4_orbitDisableList.empty();
}

void CPlayer::RemoveOrbitDisableSource(TUniqueId uid) {
  for (auto it = x9e4_orbitDisableList.begin(); it != x9e4_orbitDisableList.end();) {
    if (*it == uid) {
      it = x9e4_orbitDisableList.erase(it);
      return;
    }
    ++it;
  }
}

void CPlayer::AddOrbitDisableSource(CStateManager& mgr, TUniqueId addId) {
  if (x9e4_orbitDisableList.size() >= 5) {
    return;
  }

  for (const auto& uid : x9e4_orbitDisableList) {
    if (uid == addId) {
      return;
    }
  }

  x9e4_orbitDisableList.push_back(addId);
  ResetAimTargetPrediction(kInvalidUniqueId);
  if (!TCastToConstPtr<CScriptGrapplePoint>(mgr.GetObjectById(x310_orbitTargetId))) {
    SetOrbitTargetId(kInvalidUniqueId, mgr);
  }
}

void CPlayer::UpdateOrbitPreventionTimer(float dt) {
  if (x378_orbitPreventionTimer <= 0.f) {
    return;
  }

  x378_orbitPreventionTimer -= dt;
}

void CPlayer::UpdateOrbitModeTimer(float dt) {
  if (x304_orbitState == EPlayerOrbitState::NoOrbit && x32c_orbitModeTimer > 0.f) {
    x32c_orbitModeTimer -= dt;
    return;
  }
  x32c_orbitModeTimer = 0.f;
}

void CPlayer::UpdateOrbitZone(CStateManager& mgr) {
  if (mgr.GetPlayerState()->GetCurrentVisor() != CPlayerState::EPlayerVisor::Scan) {
    x334_orbitType = EPlayerZoneType::Ellipse;
    x338_ = 1;
    x330_orbitZoneMode = EPlayerZoneInfo::Targeting;
  } else {
    x334_orbitType = EPlayerZoneType::Box;
    x338_ = 2;
    x330_orbitZoneMode = EPlayerZoneInfo::Scan;
  }
}

void CPlayer::UpdateOrbitInput(const CFinalInput& input, CStateManager& mgr) {
  if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed || x378_orbitPreventionTimer > 0.f) {
    return;
  }

  UpdateOrbitableObjects(mgr);
  if (x304_orbitState == EPlayerOrbitState::NoOrbit) {
    x33c_orbitNextTargetId = FindOrbitTargetId(mgr);
  }

  if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::OrbitClose, input) ||
      ControlMapper::GetDigitalInput(ControlMapper::ECommands::OrbitFar, input) ||
      ControlMapper::GetDigitalInput(ControlMapper::ECommands::OrbitObject, input)) {
    switch (x304_orbitState) {
    case EPlayerOrbitState::NoOrbit:
      /* Disabled transitions directly from NoOrbit to OrbitObject for better keyboard handling */
#if 0
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::OrbitObject, input)) {
        SetOrbitTargetId(x33c_orbitNextTargetId, mgr);
        if (x310_orbitTargetId != kInvalidUniqueId) {
          if (ValidateAimTargetId(x310_orbitTargetId, mgr)) {
            ResetAimTargetPrediction(x310_orbitTargetId);
          }
          SetOrbitState(EPlayerOrbitState::OrbitObject, mgr);
          UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
        }
      } else {
#else
      m_deferredOrbitObject = ControlMapper::GetPressInput(ControlMapper::ECommands::OrbitObject, input);
#endif
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::OrbitFar, input)) {
        OrbitPoint(EPlayerOrbitType::Far, mgr);
      }
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::OrbitClose, input)) {
        OrbitPoint(EPlayerOrbitType::Close, mgr);
      }
#if 0
      }
#endif
      break;
    case EPlayerOrbitState::Grapple:
      if (x310_orbitTargetId == kInvalidUniqueId) {
        BreakGrapple(EPlayerOrbitRequest::StopOrbit, mgr);
      }
      break;
    case EPlayerOrbitState::OrbitObject:
      if (TCastToConstPtr<CScriptGrapplePoint>(mgr.GetObjectById(x310_orbitTargetId))) {
        if (ValidateCurrentOrbitTargetId(mgr) == EOrbitValidationResult::OK) {
          UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
        } else {
          BreakGrapple(EPlayerOrbitRequest::InvalidateTarget, mgr);
        }
      } else {
        const EOrbitValidationResult result = ValidateCurrentOrbitTargetId(mgr);
        if (result == EOrbitValidationResult::OK) {
          UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
        } else if (result == EOrbitValidationResult::BrokenLookAngle) {
          OrbitPoint(EPlayerOrbitType::Far, mgr);
        } else if (result == EOrbitValidationResult::ExtremeHorizonAngle) {
          SetOrbitRequest(EPlayerOrbitRequest::BadVerticalAngle, mgr);
        } else {
          ActivateOrbitSource(mgr);
        }
      }
      UpdateOrbitSelection(input, mgr);
      break;
    case EPlayerOrbitState::OrbitPoint:
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::OrbitObject, input) || m_deferredOrbitObject) {
        m_deferredOrbitObject = false;
        SetOrbitTargetId(FindOrbitTargetId(mgr), mgr);
        if (x310_orbitTargetId != kInvalidUniqueId) {
          if (ValidateAimTargetId(x310_orbitTargetId, mgr)) {
            ResetAimTargetPrediction(x310_orbitTargetId);
          }
          SetOrbitState(EPlayerOrbitState::OrbitObject, mgr);
          UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
        }
      } else {
        switch (x308_orbitType) {
        case EPlayerOrbitType::Far:
          if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::OrbitClose, input)) {
            x308_orbitType = EPlayerOrbitType::Close;
            UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
          }
          break;
        case EPlayerOrbitType::Close:
          if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::OrbitFar, input) &&
              !ControlMapper::GetDigitalInput(ControlMapper::ECommands::OrbitClose, input)) {
            x308_orbitType = EPlayerOrbitType::Far;
            UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
          }
          break;
        default:
          break;
        }
      }
      UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
      break;
    case EPlayerOrbitState::OrbitCarcass:
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::OrbitObject, input) || m_deferredOrbitObject) {
        m_deferredOrbitObject = false;
        SetOrbitTargetId(FindOrbitTargetId(mgr), mgr);
        if (x310_orbitTargetId != kInvalidUniqueId) {
          if (ValidateAimTargetId(x310_orbitTargetId, mgr)) {
            ResetAimTargetPrediction(x310_orbitTargetId);
          }
          SetOrbitState(EPlayerOrbitState::OrbitObject, mgr);
          UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
        }
      }
      UpdateOrbitSelection(input, mgr);
      break;
    case EPlayerOrbitState::ForcedOrbitObject:
      UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
      UpdateOrbitSelection(input, mgr);
      break;
    }

    if (x304_orbitState == EPlayerOrbitState::Grapple) {
      x33c_orbitNextTargetId = FindOrbitTargetId(mgr);
      if (x33c_orbitNextTargetId == x310_orbitTargetId) {
        x33c_orbitNextTargetId = kInvalidUniqueId;
      }
    }
  } else {
    switch (x304_orbitState) {
    case EPlayerOrbitState::NoOrbit:
      break;
    case EPlayerOrbitState::OrbitObject:
      if (TCastToConstPtr<CScriptGrapplePoint>(mgr.GetObjectById(x310_orbitTargetId))) {
        BreakGrapple(EPlayerOrbitRequest::Default, mgr);
      } else {
        SetOrbitRequest(EPlayerOrbitRequest::StopOrbit, mgr);
      }
      break;
    case EPlayerOrbitState::Grapple:
      if (!g_tweakPlayer->GetOrbitReleaseBreaksGrapple()) {
        x33c_orbitNextTargetId = FindOrbitTargetId(mgr);
        if (x33c_orbitNextTargetId == x310_orbitTargetId) {
          x33c_orbitNextTargetId = kInvalidUniqueId;
        }
      } else {
        BreakGrapple(EPlayerOrbitRequest::StopOrbit, mgr);
      }
      break;
    case EPlayerOrbitState::ForcedOrbitObject:
      UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
      UpdateOrbitSelection(input, mgr);
      break;
    default:
      SetOrbitRequest(EPlayerOrbitRequest::StopOrbit, mgr);
      break;
    }
  }
}

void CPlayer::ActivateOrbitSource(CStateManager& mgr) {
  switch (x390_orbitSource) {
  default:
    OrbitCarcass(mgr);
    break;
  case 1:
    SetOrbitRequest(EPlayerOrbitRequest::InvalidateTarget, mgr);
    break;
  case 2:
    if (x394_orbitingEnemy) {
      OrbitPoint(EPlayerOrbitType::Far, mgr);
    } else {
      OrbitCarcass(mgr);
    }
    break;
  }
}

void CPlayer::UpdateOrbitSelection(const CFinalInput& input, CStateManager& mgr) {
  x33c_orbitNextTargetId = FindOrbitTargetId(mgr);
  const TCastToConstPtr<CScriptGrapplePoint> curPoint = mgr.GetObjectById(x310_orbitTargetId);
  const TCastToConstPtr<CScriptGrapplePoint> nextPoint = mgr.GetObjectById(x33c_orbitNextTargetId);
  if (curPoint || (x304_orbitState == EPlayerOrbitState::Grapple && !nextPoint)) {
    x33c_orbitNextTargetId = kInvalidUniqueId;
    return;
  }

  if (ControlMapper::GetPressInput(ControlMapper::ECommands::OrbitObject, input) &&
      x33c_orbitNextTargetId != kInvalidUniqueId) {
    SetOrbitTargetId(x33c_orbitNextTargetId, mgr);
    if (ValidateAimTargetId(x310_orbitTargetId, mgr)) {
      ResetAimTargetPrediction(x310_orbitTargetId);
    }
    SetOrbitState(EPlayerOrbitState::OrbitObject, mgr);
    UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
  }
}

void CPlayer::UpdateOrbitOrientation(CStateManager& mgr) {
  if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed) {
    return;
  }

  switch (x304_orbitState) {
  case EPlayerOrbitState::OrbitPoint:
    if (x3dc_inFreeLook) {
      return;
    }
    [[fallthrough]];
  case EPlayerOrbitState::OrbitObject:
  case EPlayerOrbitState::OrbitCarcass:
  case EPlayerOrbitState::ForcedOrbitObject: {
    zeus::CVector3f playerToPoint = x314_orbitPoint - GetTranslation();
    if (!x374_orbitLockEstablished) {
      playerToPoint = mgr.GetCameraManager()->GetFirstPersonCamera()->GetTransform().basis[1];
    }
    playerToPoint.z() = 0.f;
    if (playerToPoint.canBeNormalized()) {
      zeus::CTransform xf = zeus::lookAt(zeus::skZero3f, playerToPoint);
      xf.origin = GetTranslation();
      SetTransform(xf);
    }
    break;
  }
  default:
    break;
  }
}

void CPlayer::UpdateOrbitTarget(CStateManager& mgr) {
  if (!ValidateOrbitTargetIdAndPointer(x310_orbitTargetId, mgr)) {
    SetOrbitTargetId(kInvalidUniqueId, mgr);
  }
  if (!ValidateOrbitTargetIdAndPointer(x33c_orbitNextTargetId, mgr)) {
    x33c_orbitNextTargetId = kInvalidUniqueId;
  }

  zeus::CVector3f playerToPoint = x314_orbitPoint - GetTranslation();
  playerToPoint.z() = 0.f;
  const float playerToPointMag = playerToPoint.magnitude();

  switch (x304_orbitState) {
  case EPlayerOrbitState::OrbitObject:
    if (const auto* ent = static_cast<const CActor*>(mgr.GetObjectById(x310_orbitTargetId))) {
      if (ent->GetDoTargetDistanceTest() &&
          (playerToPointMag >= GetOrbitMaxLockDistance(mgr) || playerToPointMag < 0.5f)) {
        if (playerToPointMag < 0.5f) {
          SetOrbitRequest(EPlayerOrbitRequest::BadVerticalAngle, mgr);
        } else {
          ActivateOrbitSource(mgr);
        }
      }
    }
    UpdateOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
    break;
  case EPlayerOrbitState::OrbitPoint: {
    if (g_tweakPlayer->GetOrbitFixedOffset() &&
        std::fabs(x320_orbitVector.z()) > g_tweakPlayer->GetOrbitFixedOffsetZDiff()) {
      UpdateOrbitFixedPosition();
      return;
    }
    if (playerToPointMag < CalculateOrbitMinDistance(x308_orbitType)) {
      UpdateOrbitPosition(CalculateOrbitMinDistance(x308_orbitType), mgr);
    }
    const float maxDist = g_tweakPlayer->GetOrbitMaxDistance(int(x308_orbitType));
    if (playerToPointMag > maxDist) {
      UpdateOrbitPosition(maxDist, mgr);
    }
    if (x3dd_lookButtonHeld) {
      SetOrbitPosition(g_tweakPlayer->GetOrbitNormalDistance(int(x308_orbitType)), mgr);
    }
    const zeus::CVector3f eyeToPoint = x314_orbitPoint - GetEyePosition();
    const float angleToPoint = std::asin(zeus::clamp(-1.f, std::fabs(eyeToPoint.z()) / eyeToPoint.magnitude(), 1.f));
    if ((eyeToPoint.z() >= 0.f && angleToPoint >= g_tweakPlayer->GetOrbitUpperAngle()) ||
        (eyeToPoint.z() < 0.f && angleToPoint >= g_tweakPlayer->GetOrbitLowerAngle())) {
      SetOrbitRequest(EPlayerOrbitRequest::BadVerticalAngle, mgr);
    }
    break;
  }
  case EPlayerOrbitState::OrbitCarcass: {
    if (x3dd_lookButtonHeld) {
      SetOrbitPosition(x340_, mgr);
    }
    if (playerToPointMag < CalculateOrbitMinDistance(x308_orbitType)) {
      UpdateOrbitPosition(CalculateOrbitMinDistance(x308_orbitType), mgr);
      x340_ = CalculateOrbitMinDistance(x308_orbitType);
    }
    const float maxDist = g_tweakPlayer->GetOrbitMaxDistance(int(x308_orbitType));
    if (playerToPointMag > maxDist) {
      UpdateOrbitPosition(maxDist, mgr);
      x340_ = g_tweakPlayer->GetOrbitMaxDistance(int(x308_orbitType));
    }
    break;
  }
  case EPlayerOrbitState::NoOrbit:
    SetOrbitTargetId(kInvalidUniqueId, mgr);
    break;
  default:
    break;
  }
  UpdateOrbitZPosition();
}

float CPlayer::GetOrbitMaxLockDistance(CStateManager& mgr) const {
  return mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan
             ? g_tweakPlayer->GetScanMaxLockDistance()
             : g_tweakPlayer->GetOrbitMaxLockDistance();
}

float CPlayer::GetOrbitMaxTargetDistance(CStateManager& mgr) const {
  return mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan
             ? g_tweakPlayer->GetScanMaxTargetDistance()
             : g_tweakPlayer->GetOrbitMaxTargetDistance();
}

CPlayer::EOrbitValidationResult CPlayer::ValidateOrbitTargetId(TUniqueId uid, CStateManager& mgr) const {
  if (uid == kInvalidUniqueId) {
    return EOrbitValidationResult::InvalidTarget;
  }

  const TCastToConstPtr<CActor> act = mgr.GetObjectById(uid);
  if (!act || !act->GetIsTargetable() || !act->GetActive()) {
    return EOrbitValidationResult::InvalidTarget;
  }

  if (x740_staticTimer != 0.f) {
    return EOrbitValidationResult::PlayerNotReadyToTarget;
  }

  const zeus::CVector3f eyePos = GetEyePosition();
  const zeus::CVector3f eyeToOrbit = act->GetOrbitPosition(mgr) - eyePos;
  zeus::CVector3f eyeToOrbitFlat = eyeToOrbit;
  eyeToOrbitFlat.z() = 0.f;

  if (eyeToOrbitFlat.canBeNormalized() && eyeToOrbitFlat.magnitude() > 1.f) {
    const float angleFromHorizon = std::asin(zeus::clamp(-1.f, std::fabs(eyeToOrbit.z()) / eyeToOrbit.magnitude(), 1.f));
    if ((eyeToOrbit.z() >= 0.f && angleFromHorizon >= g_tweakPlayer->GetOrbitUpperAngle()) ||
        (eyeToOrbit.z() < 0.f && angleFromHorizon >= g_tweakPlayer->GetOrbitLowerAngle())) {
      return EOrbitValidationResult::ExtremeHorizonAngle;
    }
  } else {
    return EOrbitValidationResult::ExtremeHorizonAngle;
  }

  const CPlayerState::EPlayerVisor visor = mgr.GetPlayerState()->GetCurrentVisor();
  const u8 flags = act->GetTargetableVisorFlags();
  if (visor == CPlayerState::EPlayerVisor::Combat && (flags & 1) == 0) {
    return EOrbitValidationResult::PlayerNotReadyToTarget;
  }
  if (visor == CPlayerState::EPlayerVisor::Scan && (flags & 2) == 0) {
    return EOrbitValidationResult::PlayerNotReadyToTarget;
  }
  if (visor == CPlayerState::EPlayerVisor::Thermal && (flags & 4) == 0) {
    return EOrbitValidationResult::PlayerNotReadyToTarget;
  }
  if (visor == CPlayerState::EPlayerVisor::XRay && (flags & 8) == 0) {
    return EOrbitValidationResult::PlayerNotReadyToTarget;
  }

  if (visor == CPlayerState::EPlayerVisor::Scan && act->GetAreaIdAlways() != GetAreaIdAlways()) {
    return EOrbitValidationResult::TargetingThroughDoor;
  }

  return EOrbitValidationResult::OK;
}

CPlayer::EOrbitValidationResult CPlayer::ValidateCurrentOrbitTargetId(CStateManager& mgr) {
  const TCastToConstPtr<CActor> act = mgr.GetObjectById(x310_orbitTargetId);
  if (!act || !act->GetIsTargetable() || !act->GetActive()) {
    return EOrbitValidationResult::InvalidTarget;
  }

  if (!act->GetMaterialList().HasMaterial(EMaterialTypes::Orbit)) {
    if (!act->GetMaterialList().HasMaterial(EMaterialTypes::Scannable)) {
      return EOrbitValidationResult::NonTargetableTarget;
    }
    if (mgr.GetPlayerState()->GetCurrentVisor() != CPlayerState::EPlayerVisor::Scan) {
      return EOrbitValidationResult::NonTargetableTarget;
    }
  }

  const EOrbitValidationResult type = ValidateOrbitTargetId(x310_orbitTargetId, mgr);
  if (type != EOrbitValidationResult::OK) {
    return type;
  }

  if (mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan &&
      act->GetAreaIdAlways() != GetAreaIdAlways()) {
    return EOrbitValidationResult::TargetingThroughDoor;
  }

  const TCastToConstPtr<CScriptGrapplePoint> point = mgr.GetObjectById(x310_orbitTargetId);
  if ((mgr.GetPlayerState()->GetCurrentVisor() == CPlayerState::EPlayerVisor::Scan &&
       g_tweakPlayer->GetOrbitWhileScanning()) ||
      point || act->GetAreaIdAlways() != GetAreaIdAlways()) {
    const zeus::CVector3f eyePos = GetEyePosition();
    TUniqueId bestId = kInvalidUniqueId;
    const zeus::CVector3f eyeToOrbit = act->GetOrbitPosition(mgr) - eyePos;
    if (eyeToOrbit.canBeNormalized()) {
      rstl::reserved_vector<TUniqueId, 1024> nearList;
      mgr.BuildNearList(nearList, eyePos, eyeToOrbit.normalized(), eyeToOrbit.magnitude(), OccluderFilter,
                        act.GetPtr());
      for (auto it = nearList.begin(); it != nearList.end();) {
        if (const CEntity* ent = mgr.GetObjectById(*it)) {
          if (ent->GetAreaIdAlways() != mgr.GetNextAreaId()) {
            const CGameArea* area = mgr.GetWorld()->GetAreaAlways(ent->GetAreaIdAlways());
            CGameArea::EOcclusionState occState = CGameArea::EOcclusionState::Occluded;
            if (area->IsPostConstructed()) {
              occState = area->GetOcclusionState();
            }
            if (occState == CGameArea::EOcclusionState::Occluded) {
              it = nearList.erase(it);
              continue;
            }
          }
        }
        ++it;
      }

      const CRayCastResult result = mgr.RayWorldIntersection(bestId, eyePos, eyeToOrbit.normalized(),
                                                             eyeToOrbit.magnitude(), LineOfSightFilter, nearList);
      if (result.IsValid()) {
        if (TCastToConstPtr<CScriptDoor>(mgr.ObjectById(bestId)) || point) {
          return EOrbitValidationResult::TargetingThroughDoor;
        }
      }
    }

    zeus::CVector3f eyeToOrbitFlat = eyeToOrbit;
    eyeToOrbitFlat.z() = 0.f;
    if (eyeToOrbitFlat.canBeNormalized()) {
      const float lookToOrbitAngle =
          std::acos(zeus::clamp(-1.f, eyeToOrbitFlat.normalized().dot(GetTransform().basis[1]), 1.f));
      if (x374_orbitLockEstablished) {
        if (lookToOrbitAngle >= g_tweakPlayer->GetOrbitHorizAngle()) {
          return EOrbitValidationResult::BrokenLookAngle;
        }
      } else {
        if (lookToOrbitAngle <= M_PIF / 180.f) {
          x374_orbitLockEstablished = true;
        }
      }
    } else {
      return EOrbitValidationResult::BrokenLookAngle;
    }
  }

  return EOrbitValidationResult::OK;
}

bool CPlayer::ValidateOrbitTargetIdAndPointer(TUniqueId uid, CStateManager& mgr) const {
  if (uid == kInvalidUniqueId) {
    return false;
  }
  return TCastToConstPtr<CActor>(mgr.GetObjectById(uid)).IsValid();
}

zeus::CVector3f CPlayer::GetBallPosition() const {
  return GetTranslation() + zeus::CVector3f(0.f, 0.f, g_tweakPlayer->GetPlayerBallHalfExtent());
}

zeus::CVector3f CPlayer::GetEyePosition() const { return GetTranslation() + zeus::CVector3f(0.f, 0.f, GetEyeHeight()); }

float CPlayer::GetEyeHeight() const { return x9c8_eyeZBias + (x2d8_fpBounds.max.z() - g_tweakPlayer->GetEyeOffset()); }

float CPlayer::GetUnbiasedEyeHeight() const { return x2d8_fpBounds.max.z() - g_tweakPlayer->GetEyeOffset(); }

float CPlayer::GetStepUpHeight() const {
  if (x258_movementState == EPlayerMovementState::Jump || x258_movementState == EPlayerMovementState::ApplyJump) {
    return 0.3f;
  }
  return CPhysicsActor::GetStepUpHeight();
}

float CPlayer::GetStepDownHeight() const {
  if (x258_movementState == EPlayerMovementState::Jump) {
    return -1.f;
  }
  if (x258_movementState == EPlayerMovementState::ApplyJump) {
    return 0.1f;
  }
  return CPhysicsActor::GetStepDownHeight();
}

void CPlayer::Teleport(const zeus::CTransform& xf, CStateManager& mgr, bool resetBallCam) {
  CPhysicsActor::Stop();
  zeus::CVector3f lookDir = xf.basis[1];
  if (lookDir.canBeNormalized()) {
    lookDir.normalize();
    SetTransform(zeus::lookAt(zeus::skZero3f, lookDir));
    SetTranslation(xf.origin);
    x500_lookDir = lookDir;
    x50c_moveDir = lookDir;
    x530_gunDir = lookDir;
    x524_lastPosForDirCalc = xf.origin;
    x4f8_moveSpeed = 0.f;
    x4fc_flatMoveSpeed = 0.f;
    x53c_timeMoving = 0.f;
    x4a4_moveSpeedAvg.Clear();
    x540_controlDir = lookDir;
    x54c_controlDirFlat = lookDir;
  } else {
    SetTranslation(xf.origin);
  }

  x9c5_31_stepCameraZBiasDirty = true;
  x9c8_eyeZBias = 0.f;
  x1f4_lastNonCollidingState = GetMotionState();
  SetMoveState(EPlayerMovementState::OnGround, mgr);
  zeus::CTransform eyeXf = x34_transform;
  eyeXf.origin = GetEyePosition();
  mgr.GetCameraManager()->GetFirstPersonCamera()->Reset(eyeXf, mgr);
  if (resetBallCam) {
    mgr.GetCameraManager()->GetBallCamera()->Reset(eyeXf, mgr);
  }
  ForceGunOrientation(x34_transform, mgr);
  SetOrbitRequest(EPlayerOrbitRequest::Respawn, mgr);
}

void CPlayer::BombJump(const zeus::CVector3f& pos, CStateManager& mgr) {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed &&
      x768_morphball->GetBombJumpState() != CMorphBall::EBombJumpState::BombJumpDisabled) {
    const zeus::CVector3f posToBall =
        GetTranslation() + zeus::CVector3f(0.f, 0.f, g_tweakPlayer->GetPlayerBallHalfExtent()) - pos;
    const float maxJump = g_tweakPlayer->GetBombJumpHeight();
    if (posToBall.magSquared() < maxJump * maxJump &&
        posToBall.dot(zeus::skUp) >= -g_tweakPlayer->GetPlayerBallHalfExtent()) {
      float upVel =
          std::sqrt(2.f * std::fabs(g_tweakPlayer->GetNormalGravAccel()) * g_tweakPlayer->GetBombJumpRadius());
      mgr.GetRumbleManager().Rumble(mgr, ERumbleFxId::PlayerBump, 0.3f, ERumblePriority::One);
      x2a0_ = 0.01f;
      switch (GetSurfaceRestraint()) {
      case ESurfaceRestraints::Water:
        upVel *= g_tweakPlayer->GetWaterBallJumpFactor();
        break;
      case ESurfaceRestraints::Lava:
        upVel *= g_tweakPlayer->GetLavaBallJumpFactor();
        break;
      case ESurfaceRestraints::Phazon:
        upVel *= g_tweakPlayer->GetPhazonBallJumpFactor();
        break;
      default:
        break;
      }
      SetVelocityWR(zeus::CVector3f(0.f, 0.f, upVel));
      x768_morphball->SetDamageTimer(0.1f);
      x768_morphball->CancelBoosting();
      if (x9d0_bombJumpCount > 0) {
        if (x9d0_bombJumpCount > 2) {
          x9d0_bombJumpCount = 0;
          x9d4_bombJumpCheckDelayFrames = 0;
        } else {
          ++x9d0_bombJumpCount;
        }
      } else {
        const CBallCamera* ballCam = mgr.GetCameraManager()->GetBallCamera();
        if (ballCam->GetTooCloseActorId() != kInvalidUniqueId && ballCam->GetTooCloseActorDistance() < 5.f) {
          x9d0_bombJumpCount = 1;
          x9d4_bombJumpCheckDelayFrames = 2;
        }
      }
      CSfxHandle hnd = CSfxManager::AddEmitter(SFXsam_ball_jump, GetTranslation(), zeus::skZero3f, false, false, 0x7f,
                                               kInvalidAreaId);
      ApplySubmergedPitchBend(hnd);
    }
  }
}

zeus::CTransform CPlayer::CreateTransformFromMovementDirection() const {
  zeus::CVector3f moveDir = x50c_moveDir;
  if (moveDir.canBeNormalized()) {
    moveDir.normalize();
  } else {
    moveDir = zeus::skForward;
  }

  return {zeus::CVector3f(moveDir.y(), -moveDir.x(), 0.f), moveDir, zeus::skUp, GetTranslation()};
}

const CCollisionPrimitive* CPlayer::GetCollisionPrimitive() const {
  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Morphed:
    return GetCollidableSphere();
  default:
    return CPhysicsActor::GetCollisionPrimitive();
  }
}

const CCollidableSphere* CPlayer::GetCollidableSphere() const { return &x768_morphball->GetCollidableSphere(); }

zeus::CTransform CPlayer::GetPrimitiveTransform() const { return CPhysicsActor::GetPrimitiveTransform(); }

void CPlayer::CollidedWith(TUniqueId id, const CCollisionInfoList& list, CStateManager& mgr) {
  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphed) {
    return;
  }

  x768_morphball->CollidedWith(id, list, mgr);
}

float CPlayer::GetBallMaxVelocity() const {
  return g_tweakBall->GetBallTranslationMaxSpeed(int(GetSurfaceRestraint()));
}

float CPlayer::GetActualBallMaxVelocity(float dt) const {
  const ESurfaceRestraints surf = GetSurfaceRestraint();
  const float friction = g_tweakBall->GetBallTranslationFriction(int(surf));
  const float maxSpeed = g_tweakBall->GetBallTranslationMaxSpeed(int(surf));
  const float acceleration = g_tweakBall->GetMaxBallTranslationAcceleration(int(surf));
  return -(friction * xe8_mass * maxSpeed / (acceleration * dt) - maxSpeed - friction);
}

float CPlayer::GetActualFirstPersonMaxVelocity(float dt) const {
  const ESurfaceRestraints surf = GetSurfaceRestraint();
  const float friction = g_tweakPlayer->GetPlayerTranslationFriction(int(surf));
  const float maxSpeed = g_tweakPlayer->GetPlayerTranslationMaxSpeed(int(surf));
  const float acceleration = g_tweakPlayer->GetMaxTranslationalAcceleration(int(surf));
  return -(friction * xe8_mass * maxSpeed / (acceleration * dt) - maxSpeed - friction);
}

void CPlayer::SetMoveState(EPlayerMovementState newState, CStateManager& mgr) {
  switch (newState) {
  case EPlayerMovementState::Jump:
    if (x258_movementState == EPlayerMovementState::ApplyJump) {
      CSfxHandle hnd = CSfxManager::SfxStart(SFXsam_b_jump_00, 1.f, 0.f, true, 0x7f, false, kInvalidAreaId);
      ApplySubmergedPitchBend(hnd);
      mgr.GetRumbleManager().Rumble(mgr, ERumbleFxId::PlayerBump, 0.2015f, ERumblePriority::One);
      x288_startingJumpTimeout = g_tweakPlayer->GetAllowedDoubleJumpTime();
      x290_minJumpTimeout = g_tweakPlayer->GetAllowedDoubleJumpTime() - g_tweakPlayer->GetMinDoubleJumpTime();
      x28c_sjTimer = 0.f;
    } else if (x258_movementState != EPlayerMovementState::Jump) {
      CSfxHandle hnd = CSfxManager::SfxStart(SFXsam_firstjump, 1.f, 0.f, true, 0x7f, false, kInvalidAreaId);
      ApplySubmergedPitchBend(hnd);
      x2a0_ = 0.01f;
      x288_startingJumpTimeout = g_tweakPlayer->GetAllowedJumpTime();
      x290_minJumpTimeout = g_tweakPlayer->GetAllowedJumpTime() - g_tweakPlayer->GetMinJumpTime();
      if (mgr.GetPlayerState()->GetItemAmount(CPlayerState::EItemType::SpaceJumpBoots) != 0) {
        x28c_sjTimer = g_tweakPlayer->GetMaxDoubleJumpWindow();
      } else {
        x28c_sjTimer = 0.f;
      }
      if (x294_jumpCameraTimer <= 0.f && x29c_fallCameraTimer <= 0.f && !x3dc_inFreeLook && !x3dd_lookButtonHeld) {
        x294_jumpCameraTimer = 0.01f;
        x2a4_cancelCameraPitch = false;
      }
    }
    x258_movementState = EPlayerMovementState::Jump;
    x2ac_surfaceRestraint = ESurfaceRestraints::Air;
    x2a8_timeSinceJump = 0.f;
    break;
  case EPlayerMovementState::Falling:
    if (x258_movementState == EPlayerMovementState::OnGround) {
      x288_startingJumpTimeout = g_tweakPlayer->GetAllowedLedgeTime();
      x258_movementState = EPlayerMovementState::Falling;
      x2a0_ = 0.01f;
      if (g_tweakPlayer->GetFallingDoubleJump()) {
        x28c_sjTimer = g_tweakPlayer->GetMaxDoubleJumpWindow();
      } else {
        x28c_sjTimer = 0.f;
      }
    }
    break;
  case EPlayerMovementState::FallingMorphed:
    x258_movementState = EPlayerMovementState::FallingMorphed;
    x2ac_surfaceRestraint = ESurfaceRestraints::Normal;
    break;
  case EPlayerMovementState::OnGround:
    x300_fallingTime = 0.f;
    x258_movementState = EPlayerMovementState::OnGround;
    x288_startingJumpTimeout = 0.f;
    x28c_sjTimer = 0.f;
    x2ac_surfaceRestraint = ESurfaceRestraints::Normal;
    if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
      AddMaterial(EMaterialTypes::GroundCollider);
    }
    x294_jumpCameraTimer = 0.f;
    x29c_fallCameraTimer = 0.f;
    x2a4_cancelCameraPitch = false;
    x298_jumpPresses = 0;
    break;
  case EPlayerMovementState::ApplyJump:
    x288_startingJumpTimeout = 0.f;
    if (x258_movementState != EPlayerMovementState::ApplyJump) {
      x258_movementState = EPlayerMovementState::ApplyJump;
      if (x294_jumpCameraTimer <= x288_startingJumpTimeout && x29c_fallCameraTimer <= x288_startingJumpTimeout &&
          !x3dc_inFreeLook && !x3dd_lookButtonHeld) {
        x29c_fallCameraTimer = 0.01f;
        x2a4_cancelCameraPitch = false;
      }
    }
    x2ac_surfaceRestraint = ESurfaceRestraints::Air;
    break;
  }
}

float CPlayer::JumpInput(const CFinalInput& input, CStateManager& mgr) {
  if (IsMorphBallTransitioning()) {
    return GetGravity() * xe8_mass;
  }

  float jumpFactor = 1.f;
  if (!mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::GravitySuit)) {
    const ESurfaceRestraints restraints = GetSurfaceRestraint();
    switch (restraints) {
    case ESurfaceRestraints::Water:
      jumpFactor = g_tweakPlayer->GetWaterJumpFactor();
      break;
    case ESurfaceRestraints::Lava:
      jumpFactor = g_tweakPlayer->GetLavaJumpFactor();
      break;
    case ESurfaceRestraints::Phazon:
      jumpFactor = g_tweakPlayer->GetPhazonJumpFactor();
      break;
    default:
      break;
    }
  }

  const float vJumpAccel = g_tweakPlayer->GetVerticalJumpAccel();
  const float hJumpAccel = g_tweakPlayer->GetHorizontalJumpAccel();
  float doubleJumpImpulse = g_tweakPlayer->GetDoubleJumpImpulse();
  float vDoubleJumpAccel = g_tweakPlayer->GetVerticalDoubleJumpAccel();
  float hDoubleJumpAccel = g_tweakPlayer->GetHorizontalDoubleJumpAccel();

  if (x37c_sidewaysDashing) {
    doubleJumpImpulse = g_tweakPlayer->GetSidewaysDoubleJumpImpulse();
    vDoubleJumpAccel = g_tweakPlayer->GetSidewaysVerticalDoubleJumpAccel();
    hDoubleJumpAccel = g_tweakPlayer->GetSidewaysHorizontalDoubleJumpAccel();
  }

  if (x828_distanceUnderWater >= 0.8f * GetEyeHeight()) {
    doubleJumpImpulse *= jumpFactor;
  }

  if (x258_movementState == EPlayerMovementState::ApplyJump) {
    if (g_tweakPlayer->GetMaxDoubleJumpWindow() - g_tweakPlayer->GetMinDoubleJumpWindow() >= x28c_sjTimer &&
        0.f < x28c_sjTimer && ControlMapper::GetPressInput(ControlMapper::ECommands::JumpOrBoost, input)) {
      SetMoveState(EPlayerMovementState::Jump, mgr);
      x384_dashTimer = 0.f;
      x380_strafeInputAtDash = StrafeInput(input);
      if (g_tweakPlayer->GetImpulseDoubleJump()) {
        const zeus::CVector3f impulse(0.f, 0.f, (doubleJumpImpulse - x138_velocity.z()) * xe8_mass);
        ApplyImpulseWR(impulse, zeus::CAxisAngle());
      }

      float forwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Forward, input);
      const float backwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Backward, input);
      if (forwards < backwards) {
        forwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Backward, input);
      }
      return ((vDoubleJumpAccel - (vDoubleJumpAccel - hDoubleJumpAccel) * forwards) * xe8_mass) * jumpFactor;
    }

    return GetGravity() * xe8_mass;
  }

  if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::JumpOrBoost, input) ||
      (x258_movementState == EPlayerMovementState::Jump && x290_minJumpTimeout <= x288_startingJumpTimeout)) {
    if (x258_movementState != EPlayerMovementState::Jump) {
      if (ControlMapper::GetPressInput(ControlMapper::ECommands::JumpOrBoost, input)) {
        SetMoveState(EPlayerMovementState::Jump, mgr);
        return (vJumpAccel * xe8_mass) * jumpFactor;
      }
      return 0.f;
    }
    float forwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Forward, input);
    const float backwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Backward, input);
    if (forwards < backwards) {
      forwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Backward, input);
    }
    return ((vJumpAccel - (vJumpAccel - hJumpAccel) * forwards) * xe8_mass) * jumpFactor;
  }

  if (x258_movementState == EPlayerMovementState::Jump) {
    SetMoveState(EPlayerMovementState::ApplyJump, mgr);
  }

  return 0.f;
}

float CPlayer::TurnInput(const CFinalInput& input) const {
  if (x304_orbitState == EPlayerOrbitState::OrbitObject || x304_orbitState == EPlayerOrbitState::Grapple) {
    return 0.f;
  }
  if (IsMorphBallTransitioning()) {
    return 0.f;
  }

  float left = ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnLeft, input);
  float right = ControlMapper::GetAnalogInput(ControlMapper::ECommands::TurnRight, input);

  if (g_tweakPlayer->GetFreeLookTurnsPlayer()) {
    if (!g_tweakPlayer->GetHoldButtonsForFreeLook() || x3dd_lookButtonHeld) {
      if (left < 0.01f && right < 0.01f) {
        left = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookLeft, input);
        right = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookRight, input);
      }
    }
  } else {
    if (!g_tweakPlayer->GetHoldButtonsForFreeLook() || x3dd_lookButtonHeld) {
      if (left < 0.01f && right < 0.01f) {
        const float f31 = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookLeft, input);
        const float f1 = ControlMapper::GetAnalogInput(ControlMapper::ECommands::LookRight, input);
        if (f31 > 0.01f || f1 > 0.01f) {
          return 0.f;
        }
      }
    }
  }

  right = left - right;
  if (x32c_orbitModeTimer > 0.f) {
    right *= (1.f - zeus::clamp(0.f, x32c_orbitModeTimer / g_tweakPlayer->GetOrbitModeTimer(), 1.f) * 0.5f);
  }
  return zeus::clamp(-1.f, right, 1.f);
}

float CPlayer::StrafeInput(const CFinalInput& input) const {
  if (IsMorphBallTransitioning() || x304_orbitState == EPlayerOrbitState::NoOrbit) {
    return 0.f;
  }
  return ControlMapper::GetAnalogInput(ControlMapper::ECommands::StrafeRight, input) -
         ControlMapper::GetAnalogInput(ControlMapper::ECommands::StrafeLeft, input);
}

float CPlayer::ForwardInput(const CFinalInput& input, float turnInput) const {
  float forwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Forward, input);
  float backwards = ControlMapper::GetAnalogInput(ControlMapper::ECommands::Backward, input);

  if (x2f8_morphBallState != EPlayerMorphBallState::Unmorphed || InGrappleJumpCooldown()) {
    backwards = 0.f;
  }

  if (x2f8_morphBallState == EPlayerMorphBallState::Morphing && x584_ballTransitionAnim == 2) {
    forwards = 0.f;
  }

  if (x2f8_morphBallState == EPlayerMorphBallState::Unmorphing && x584_ballTransitionAnim == 5) {
    forwards = 0.f;
  }

  if (forwards >= 0.001f) {
    forwards = zeus::clamp(-1.f, forwards / 0.8f, 1.f);
    if (std::fabs(std::atan2(std::fabs(turnInput), forwards)) < M_PIF / 3.6f) {
      const zeus::CVector3f x50(std::fabs(turnInput), forwards, 0.f);
      if (x50.canBeNormalized()) {
        forwards = x50.magnitude();
      }
    }
  }
  if (backwards >= 0.001f) {
    backwards = zeus::clamp(-1.f, backwards / 0.8f, 1.f);
    if (std::fabs(std::atan2(std::fabs(turnInput), backwards)) < M_PIF / 3.6f) {
      const zeus::CVector3f x5c(std::fabs(turnInput), backwards, 0.f);
      if (x5c.canBeNormalized()) {
        backwards = x5c.magnitude();
      }
    }
  }

  if (!g_tweakPlayer->GetMoveDuringFreeLook()) {
    zeus::CVector3f velFlat = x138_velocity;
    velFlat.z() = 0.f;
    if (x3dc_inFreeLook || x3dd_lookButtonHeld) {
      if (x258_movementState == EPlayerMovementState::OnGround || std::fabs(velFlat.magnitude()) < 0.00001f) {
        return 0.f;
      }
    }
  }

  return zeus::clamp(-1.f, forwards - backwards * g_tweakPlayer->GetBackwardsForceMultiplier(), 1.f);
}

zeus::CVector3f CPlayer::CalculateLeftStickEdgePosition(float strafeInput, float forwardInput) const {
  float f31 = -1.f;
  float f30 = -0.555f;
  float f29 = 0.555f;

  if (strafeInput >= 0.f) {
    f31 = -f31;
    f30 = -f30;
  }

  if (forwardInput < 0.f) {
    f29 = -0.555f;
  }

  const float f4 = zeus::clamp(-1.f, std::atan(std::fabs(forwardInput) / std::fabs(strafeInput)) / (M_PIF / 4.f), 1.f);
  return zeus::CVector3f(f30 - f31, f29, 0.f) * f4 + zeus::CVector3f(f31, 0.f, 0.f);
}

bool CPlayer::SidewaysDashAllowed(float strafeInput, float forwardInput, const CFinalInput& input,
                                  CStateManager& mgr) const {
  if (x9c5_28_slidingOnWall || x9c5_29_hitWall || x304_orbitState != EPlayerOrbitState::OrbitObject) {
    return false;
  }

  if (g_tweakPlayer->GetDashOnButtonRelease()) {
    if (x304_orbitState != EPlayerOrbitState::NoOrbit && g_tweakPlayer->GetDashEnabled() &&
        x288_startingJumpTimeout > 0.f &&
        !ControlMapper::GetDigitalInput(ControlMapper::ECommands::JumpOrBoost, input) &&
        x388_dashButtonHoldTime < g_tweakPlayer->GetDashButtonHoldCancelTime() &&
        std::fabs(strafeInput) >= std::fabs(forwardInput) &&
        std::fabs(strafeInput) > g_tweakPlayer->GetDashStrafeInputThreshold()) {
      return true;
    }
  } else {
    if (x304_orbitState != EPlayerOrbitState::NoOrbit && g_tweakPlayer->GetDashEnabled() &&
        ControlMapper::GetPressInput(ControlMapper::ECommands::JumpOrBoost, input) && x288_startingJumpTimeout > 0.f &&
        std::fabs(strafeInput) >= std::fabs(forwardInput) && std::fabs(strafeInput) > 0.01f) {
      const float threshold = std::sqrt(strafeInput * strafeInput + forwardInput * forwardInput) /
                              CalculateLeftStickEdgePosition(strafeInput, forwardInput).magnitude();
      if (threshold >= g_tweakPlayer->GetDashStrafeInputThreshold()) {
        return true;
      }
    }
  }

  return false;
}

void CPlayer::FinishSidewaysDash() {
  if (x37c_sidewaysDashing) {
    x38c_doneSidewaysDashing = true;
  }
  x37c_sidewaysDashing = false;
  x380_strafeInputAtDash = 0.f;
  x384_dashTimer = 0.f;
}

void CPlayer::ComputeDash(const CFinalInput& input, float dt, CStateManager& mgr) {
  const float strafeInput = StrafeInput(input);
  const float forwardInput = ForwardInput(input, TurnInput(input));
  zeus::CVector3f orbitPointFlattened(x314_orbitPoint.x(), x314_orbitPoint.y(), GetTranslation().z());
  const zeus::CVector3f orbitToPlayer = GetTranslation() - orbitPointFlattened;
  if (!orbitToPlayer.canBeNormalized()) {
    return;
  }

  zeus::CVector3f useOrbitToPlayer = orbitToPlayer;
  const ESurfaceRestraints restraints = GetSurfaceRestraint();
  float strafeVel = dt * skStrafeDistances[size_t(restraints)];
  if (ControlMapper::GetDigitalInput(ControlMapper::ECommands::JumpOrBoost, input)) {
    x388_dashButtonHoldTime += dt;
  }

  if (!x37c_sidewaysDashing) {
    if (SidewaysDashAllowed(strafeInput, forwardInput, input, mgr)) {
      x37c_sidewaysDashing = true;
      x380_strafeInputAtDash = strafeInput;
      x38c_doneSidewaysDashing = true;
      x384_dashTimer = 0.f;
      zeus::CVector3f vel = x138_velocity;
      if (vel.z() > 0.f) {
        vel.z() *= 0.1f;
        if (!x9c5_28_slidingOnWall) {
          SetVelocityWR(vel);
          x778_dashSfx = CSfxManager::SfxStart(SFXsam_dash, 1.f, 0.f, true, 0x7f, false, kInvalidAreaId);
          ApplySubmergedPitchBend(x778_dashSfx);
          mgr.GetRumbleManager().Rumble(mgr, ERumbleFxId::PlayerBump, 0.24375f, ERumblePriority::One);
        }
      }
    }
    strafeVel *= strafeInput;
  } else {
    x384_dashTimer += dt;
    if (x258_movementState == EPlayerMovementState::OnGround || x384_dashTimer >= x3a0_dashDuration ||
        x9c5_28_slidingOnWall || x9c5_29_hitWall || x304_orbitState != EPlayerOrbitState::OrbitObject) {
      FinishSidewaysDash();
      strafeVel *= strafeInput;
      CSfxManager::RemoveEmitter(x778_dashSfx);
    } else {
      if (x39c_noStrafeDashBlend) {
        strafeVel = dt * skDashStrafeDistances[size_t(restraints)] * x398_dashSpeedMultiplier;
      } else {
        strafeVel = ((skDashStrafeDistances[size_t(restraints)] - skStrafeDistances[size_t(restraints)]) *
                         (1.f - zeus::clamp(-1.f, x384_dashTimer / x3a4_strafeDashBlendDuration, 1.f)) +
                     skStrafeDistances[size_t(restraints)]) *
                    x398_dashSpeedMultiplier * dt;
      }
      if (x380_strafeInputAtDash < 0.f) {
        strafeVel = -strafeVel;
      }
    }
  }

  const float f3 = strafeVel / orbitToPlayer.magnitude();
  const float f2 = dt * (x37c_sidewaysDashing ? M_PIF : (M_PIF * 2.f / 3.f));
  useOrbitToPlayer = zeus::CQuaternion::fromAxisAngle(zeus::skUp, zeus::clamp(-f2, f3, f2)).transform(orbitToPlayer);
  orbitPointFlattened += useOrbitToPlayer;
  if (!ControlMapper::GetDigitalInput(ControlMapper::ECommands::JumpOrBoost, input)) {
    x388_dashButtonHoldTime = 0.f;
  }

  strafeVel = skOrbitForwardDistances[size_t(restraints)] * forwardInput * dt;
  orbitPointFlattened += -useOrbitToPlayer.normalized() * strafeVel;
  const zeus::CVector2f velFlat(x138_velocity.x(), x138_velocity.y());
  zeus::CVector3f newVelocity = (orbitPointFlattened - GetTranslation()) / dt;
  const zeus::CVector3f velDelta(newVelocity.x() - x138_velocity.x(), newVelocity.y() - x138_velocity.y(), 0.f);
  strafeVel = velDelta.magnitude();

  if (strafeVel <= FLT_EPSILON) {
    return;
  }

  const float accel = dt * GetAcceleration();
  newVelocity = velDelta / strafeVel * accel * zeus::clamp(-1.f, strafeVel / accel, 1.f) + x138_velocity;
  if (x9c5_28_slidingOnWall) {
    return;
  }

  SetVelocityWR(newVelocity);
}

void CPlayer::ComputeMovement(const CFinalInput& input, CStateManager& mgr, float dt) {
  ESurfaceRestraints restraints = GetSurfaceRestraint();

  const float jumpInput = JumpInput(input, mgr);
  float zRotateInput = TurnInput(input);
  const float forwardInput = ForwardInput(input, zRotateInput);
  SetVelocityWR(GetDampedClampedVelocityWR());
  float turnSpeedMul = g_tweakPlayer->GetTurnSpeedMultiplier();
  if (g_tweakPlayer->GetFreeLookTurnsPlayer()) {
    if (!g_tweakPlayer->GetHoldButtonsForFreeLook() || x3dd_lookButtonHeld) {
      turnSpeedMul = g_tweakPlayer->GetFreeLookTurnSpeedMultiplier();
    }
  }

  if (x304_orbitState == EPlayerOrbitState::NoOrbit ||
      (x3dd_lookButtonHeld && x304_orbitState != EPlayerOrbitState::OrbitObject &&
       x304_orbitState != EPlayerOrbitState::Grapple)) {
    if (std::fabs(zRotateInput) < 0.00001f) {
      const float frictionZAngVel =
          g_tweakPlayer->GetPlayerRotationFriction(int(restraints)) * GetAngularVelocityOR().getVector().z();
      SetAngularVelocityOR({0.f, 0.f, frictionZAngVel});
    }

    const float curZAngVel = GetAngularVelocityOR().getVector().z();
    const float maxZAngVel = g_tweakPlayer->GetPlayerRotationMaxSpeed(int(restraints)) * turnSpeedMul;
    if (curZAngVel > maxZAngVel) {
      SetAngularVelocityOR({0.f, 0.f, maxZAngVel});
    } else if (-curZAngVel > maxZAngVel) {
      SetAngularVelocityOR({0.f, 0.f, -maxZAngVel});
    }
  }

  float f26 = g_tweakPlayer->GetPlayerRotationMaxSpeed(int(restraints)) * zRotateInput * turnSpeedMul;
  f26 -= GetAngularVelocityOR().getVector().z();
  const float remainVel = zeus::clamp(
      0.f, std::fabs(f26) / (turnSpeedMul * g_tweakPlayer->GetPlayerRotationMaxSpeed(int(restraints))), 1.f);
  if (f26 < 0.f) {
    zRotateInput = remainVel * -g_tweakPlayer->GetMaxRotationalAcceleration(int(restraints));
  } else {
    zRotateInput = remainVel * g_tweakPlayer->GetMaxRotationalAcceleration(int(restraints));
  }

  float forwardForce;
  if (std::fabs(forwardInput) >= 0.00001f) {
    const float maxSpeed = g_tweakPlayer->GetPlayerTranslationMaxSpeed(int(restraints));
    const float calcSpeed = g_tweakPlayer->GetPlayerTranslationFriction(int(restraints)) * xe8_mass /
                            (dt * g_tweakPlayer->GetMaxTranslationalAcceleration(int(restraints))) * maxSpeed;
    const float f28 = (forwardInput > 0.f ? 1.f : -1.f) * calcSpeed + (maxSpeed - calcSpeed) * forwardInput;
    forwardForce = zeus::clamp(-1.f, (f28 - x34_transform.transposeRotate(x138_velocity).y()) / maxSpeed, 1.f) *
                   g_tweakPlayer->GetMaxTranslationalAcceleration(int(restraints));
  } else {
    forwardForce = 0.f;
  }

  if (x304_orbitState != EPlayerOrbitState::NoOrbit && x3dd_lookButtonHeld) {
    forwardForce = 0.f;
  }

  if (x304_orbitState == EPlayerOrbitState::NoOrbit || x3dd_lookButtonHeld) {
    ApplyForceOR({0.f, forwardForce, jumpInput}, zeus::CAxisAngle());
    if (zRotateInput != 0.f) {
      ApplyForceOR(zeus::skZero3f, zeus::CAxisAngle({0.f, 0.f, 1.f}, zRotateInput));
    }
    if (x37c_sidewaysDashing) {
      x38c_doneSidewaysDashing = true;
    }
    x37c_sidewaysDashing = false;
    x380_strafeInputAtDash = 0.f;
    x384_dashTimer = 0.f;
  } else {
    switch (x304_orbitState) {
    case EPlayerOrbitState::OrbitObject:
    case EPlayerOrbitState::OrbitPoint:
    case EPlayerOrbitState::OrbitCarcass:
    case EPlayerOrbitState::ForcedOrbitObject:
      if (!InGrappleJumpCooldown()) {
        ComputeDash(input, dt, mgr);
      }
      break;
    default:
      break;
    }
    ApplyForceOR({0.f, 0.f, jumpInput}, zeus::CAxisAngle());
  }

  if (x3dc_inFreeLook || x3dd_lookButtonHeld) {
    if (!x9c5_28_slidingOnWall && x258_movementState == EPlayerMovementState::OnGround) {
      const zeus::CVector3f revVelFlat(-x138_velocity.x(), -x138_velocity.y(), 0.f);
      const float revVelFlatMag = revVelFlat.magnitude();
      if (revVelFlatMag > FLT_EPSILON) {
        const float accel = 0.2f * dt * GetAcceleration();
        const float damp = zeus::clamp(-1.f, revVelFlatMag / accel, 1.f);
        SetVelocityWR(revVelFlat / revVelFlatMag * accel * damp + x138_velocity);
      }
    }
  }

  x9c5_29_hitWall = false;
  if (x2d4_accelerationChangeTimer > 0.f) {
    x2d0_curAcceleration = 0;
  } else {
    x2d0_curAcceleration += 1;
  }
  x2d4_accelerationChangeTimer -= dt;
  x2d4_accelerationChangeTimer = std::max(0.f, x2d4_accelerationChangeTimer);
}

float CPlayer::GetWeight() const { return xe8_mass * -GetGravity(); }

zeus::CVector3f CPlayer::GetDampedClampedVelocityWR() const {
  zeus::CVector3f localVel = x34_transform.transposeRotate(x138_velocity);
  if ((x258_movementState != EPlayerMovementState::ApplyJump || GetSurfaceRestraint() != ESurfaceRestraints::Air) &&
      x304_orbitState == EPlayerOrbitState::NoOrbit) {
    const float friction = g_tweakPlayer->GetPlayerTranslationFriction(int(GetSurfaceRestraint()));
    if (localVel.y() > 0.f) {
      localVel.y() = std::max(0.f, localVel.y() - friction);
    } else {
      localVel.y() = std::min(0.f, localVel.y() + friction);
    }
    if (localVel.x() > 0.f) {
      localVel.x() = std::max(0.f, localVel.x() - friction);
    } else {
      localVel.x() = std::min(0.f, localVel.x() + friction);
    }
  }

  const float maxSpeed = g_tweakPlayer->GetPlayerTranslationMaxSpeed(int(GetSurfaceRestraint()));
  localVel.y() = zeus::clamp(-maxSpeed, float(localVel.y()), maxSpeed);
  if (x258_movementState == EPlayerMovementState::OnGround) {
    localVel.z() = 0.f;
  }
  return x34_transform.rotate(localVel);
}

const CScriptWater* CPlayer::GetVisorRunoffEffect(const CStateManager& mgr) const {
  if (xc4_fluidId == kInvalidUniqueId) {
    return nullptr;
  }
  return TCastToConstPtr<CScriptWater>(mgr.GetObjectById(xc4_fluidId)).GetPtr();
}

void CPlayer::SetMorphBallState(EPlayerMorphBallState state, CStateManager& mgr) {
  if (x2f8_morphBallState == EPlayerMorphBallState::Morphed && state != EPlayerMorphBallState::Morphed) {
    x9c5_26_ = x9c4_31_inWaterMovement;
  }

  x2f8_morphBallState = state;
  xf9_standardCollider = state == EPlayerMorphBallState::Morphed;

  switch (x2f8_morphBallState) {
  case EPlayerMorphBallState::Unmorphed:
    if (x9c5_26_ && mgr.GetCameraManager()->GetFluidCounter() == 0) {
      if (const auto* water = GetVisorRunoffEffect(mgr)) {
        if (const auto& effect = water->GetUnmorphVisorRunoffEffect()) {
          auto* sheets = new CHUDBillboardEffect(
              *effect, {}, mgr.AllocateUniqueId(), true, "WaterSheets", CHUDBillboardEffect::GetNearClipDistance(mgr),
              CHUDBillboardEffect::GetScaleForPOV(mgr), zeus::skWhite, zeus::skOne3f, zeus::skZero3f);
          mgr.AddObject(sheets);
        }
        CSfxHandle hnd =
            CSfxManager::SfxStart(water->GetUnmorphVisorRunoffSfx(), 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
        ApplySubmergedPitchBend(hnd);
      }
    }
    break;
  case EPlayerMorphBallState::Morphed:
  case EPlayerMorphBallState::Morphing:
    x768_morphball->LoadMorphBallModel(mgr);
    break;
  default:
    break;
  }
}

bool CPlayer::CanLeaveMorphBallState(CStateManager& mgr, zeus::CVector3f& pos) const {
  if (x768_morphball->IsProjectile() || !x590_leaveMorphballAllowed ||
      (IsUnderBetaMetroidAttack(mgr) && x2f8_morphBallState == EPlayerMorphBallState::Morphed)) {
    return false;
  }

  if (!x9c4_28_canLeaveMorphBall) {
    return false;
  }

  constexpr CMaterialFilter filter = CMaterialFilter::MakeInclude({EMaterialTypes::Solid});
  const zeus::CAABox aabb(x2d8_fpBounds.min - zeus::CVector3f(1.f) + GetTranslation(),
                          x2d8_fpBounds.max + zeus::CVector3f(1.f) + GetTranslation());
  rstl::reserved_vector<TUniqueId, 1024> nearList;
  mgr.BuildColliderList(nearList, *this, aabb);
  const zeus::CAABox& baseAABB = GetBaseBoundingBox();
  pos = zeus::skZero3f;

  for (int i = 0; i < 8; ++i) {
    const zeus::CAABox aabb2(baseAABB.min + pos + GetTranslation(), baseAABB.max + pos + GetTranslation());
    const CCollidableAABox cAABB(aabb2, CMaterialList());
    if (!CGameCollision::DetectCollisionBoolean(mgr, cAABB, zeus::CTransform(), filter, nearList)) {
      return true;
    }
    pos.z() += 0.1f;
  }

  return false;
}

void CPlayer::SetHudDisable(float staticTimer, float outSpeed, float inSpeed) {
  x740_staticTimer = staticTimer;
  x744_staticOutSpeed = outSpeed;
  x748_staticInSpeed = inSpeed;

  if (x744_staticOutSpeed != 0.f) {
    return;
  }

  if (x740_staticTimer == 0.f) {
    x74c_visorStaticAlpha = 1.f;
  } else {
    x74c_visorStaticAlpha = 0.f;
  }
}

void CPlayer::SetIntoBallReadyAnimation(CStateManager& mgr) {
  constexpr CAnimPlaybackParms parms(2, -1, 1.f, true);
  x64_modelData->GetAnimationData()->SetAnimation(parms, false);
  x64_modelData->GetAnimationData()->EnableLooping(false);
  x64_modelData->AdvanceAnimation(0.f, mgr, kInvalidAreaId, true);
  x64_modelData->GetAnimationData()->EnableAnimation(false);
}

void CPlayer::LeaveMorphBallState(CStateManager& mgr) {
  x730_transitionModels.clear();
  AddMaterial(EMaterialTypes::GroundCollider, mgr);
  x150_momentum = zeus::skZero3f;
  SetMorphBallState(EPlayerMorphBallState::Unmorphed, mgr);
  SetHudDisable(FLT_EPSILON, 0.f, 2.f);
  SetHudDisable(FLT_EPSILON, 0.f, 2.f);
  SetIntoBallReadyAnimation(mgr);
  CPhysicsActor::Stop();
  x3e4_freeLookYawAngle = 0.f;
  x3e8_horizFreeLookAngleVel = 0.f;
  x3ec_freeLookPitchAngle = 0.f;
  x3f0_vertFreeLookAngleVel = 0.f;
  x768_morphball->LeaveMorphBallState(mgr);
  mgr.GetCameraManager()->SetPlayerCamera(mgr, mgr.GetCameraManager()->GetFirstPersonCamera()->GetUniqueId());
  mgr.GetCameraManager()->GetBallCamera()->SetState(CBallCamera::EBallCameraState::Default, mgr);
  SetCameraState(EPlayerCameraState::FirstPerson, mgr);
  mgr.GetCameraManager()->GetFirstPersonCamera()->DeferBallTransitionProcessing();
  mgr.GetCameraManager()->GetFirstPersonCamera()->Think(0.f, mgr);
  ForceGunOrientation(x34_transform, mgr);
  DrawGun(mgr);
}

bool CPlayer::CanEnterMorphBallState(CStateManager& mgr, float f1) const {
  if (x3b8_grappleState != EGrappleState::None ||
      (IsUnderBetaMetroidAttack(mgr) && x2f8_morphBallState == EPlayerMorphBallState::Unmorphed)) {
    return false;
  }
  return x9c4_27_canEnterMorphBall;
}

void CPlayer::EnterMorphBallState(CStateManager& mgr) {
  SetMorphBallState(EPlayerMorphBallState::Morphed, mgr);
  RemoveMaterial(EMaterialTypes::GroundCollider, mgr);
  x730_transitionModels.clear();
  SetAngularVelocityOR(zeus::CAxisAngle(
      zeus::CVector3f(x138_velocity.magnitude() / g_tweakPlayer->GetPlayerBallHalfExtent(), 0.f, 0.f)));
  x768_morphball->EnterMorphBallState(mgr);
  x768_morphball->TakeDamage(-1.f);
  x768_morphball->SetDamageTimer(0.f);
  mgr.GetPlayerState()->StartTransitionToVisor(CPlayerState::EPlayerVisor::Combat);
}

void CPlayer::ActivateMorphBallCamera(CStateManager& mgr) {
  SetCameraState(EPlayerCameraState::Ball, mgr);
  mgr.GetCameraManager()->GetBallCamera()->SetState(CBallCamera::EBallCameraState::Default, mgr);
}

void CPlayer::UpdateCinematicState(CStateManager& mgr) {
  if (mgr.GetCameraManager()->IsInCinematicCamera()) {
    if (x2f4_cameraState != EPlayerCameraState::Spawned) {
      x2fc_spawnedMorphBallState = x2f8_morphBallState;
      if (x2fc_spawnedMorphBallState == EPlayerMorphBallState::Unmorphing) {
        x2fc_spawnedMorphBallState = EPlayerMorphBallState::Unmorphed;
      }
      if (x2fc_spawnedMorphBallState == EPlayerMorphBallState::Morphing) {
        x2fc_spawnedMorphBallState = EPlayerMorphBallState::Morphed;
      }
      SetCameraState(EPlayerCameraState::Spawned, mgr);
    }
  } else {
    if (x2f4_cameraState == EPlayerCameraState::Spawned) {
      if (x2fc_spawnedMorphBallState == x2f8_morphBallState) {
        switch (x2fc_spawnedMorphBallState) {
        case EPlayerMorphBallState::Morphed:
          SetCameraState(EPlayerCameraState::Ball, mgr);
          break;
        case EPlayerMorphBallState::Unmorphed:
          SetCameraState(EPlayerCameraState::FirstPerson, mgr);
          if (mgr.GetPlayerState()->GetCurrentVisor() != CPlayerState::EPlayerVisor::Scan) {
            ForceGunOrientation(x34_transform, mgr);
            DrawGun(mgr);
          }
          break;
        default:
          break;
        }
      } else {
        CPhysicsActor::Stop();
        SetOrbitRequest(EPlayerOrbitRequest::Respawn, mgr);
        switch (x2fc_spawnedMorphBallState) {
        case EPlayerMorphBallState::Unmorphed: {
          zeus::CVector3f vec;
          if (CanLeaveMorphBallState(mgr, vec)) {
            SetTranslation(GetTranslation() + vec);
            LeaveMorphBallState(mgr);
            SetCameraState(EPlayerCameraState::FirstPerson, mgr);
            ForceGunOrientation(x34_transform, mgr);
            DrawGun(mgr);
          }
          break;
        }
        case EPlayerMorphBallState::Morphed:
          EnterMorphBallState(mgr);
          ActivateMorphBallCamera(mgr);
          mgr.GetCameraManager()->SetupBallCamera(mgr);
          mgr.GetCameraManager()->GetBallCamera()->Reset(CreateTransformFromMovementDirection(), mgr);
          break;
        default:
          break;
        }
      }
    }
  }
}

void CPlayer::SetCameraState(EPlayerCameraState camState, CStateManager& stateMgr) {
  if (x2f4_cameraState == camState) {
    return;
  }

  x2f4_cameraState = camState;

  CCameraManager* camMgr = stateMgr.GetCameraManager();
  switch (camState) {
  case EPlayerCameraState::FirstPerson:
    camMgr->SetCurrentCameraId(camMgr->GetFirstPersonCamera()->GetUniqueId(), stateMgr);
    x768_morphball->SetBallLightActive(stateMgr, false);
    break;
  case EPlayerCameraState::Ball:
  case EPlayerCameraState::Transitioning:
    camMgr->SetCurrentCameraId(camMgr->GetBallCamera()->GetUniqueId(), stateMgr);
    x768_morphball->SetBallLightActive(stateMgr, true);
    break;
  case EPlayerCameraState::Two:
    break;
  case EPlayerCameraState::Spawned: {
    bool ballLight = false;
    if (const TCastToConstPtr<CCinematicCamera> cineCam = camMgr->GetCurrentCamera(stateMgr)) {
      ballLight = x2f8_morphBallState == EPlayerMorphBallState::Morphed && cineCam->GetFlags() & 0x40;
    }
    x768_morphball->SetBallLightActive(stateMgr, ballLight);
    break;
  }
  }
}

bool CPlayer::IsEnergyLow(const CStateManager& mgr) const {
  const float lowThreshold =
      mgr.GetPlayerState()->GetItemCapacity(CPlayerState::EItemType::EnergyTanks) < 4 ? 30.f : 100.f;
  return GetHealthInfo(mgr)->GetHP() < lowThreshold;
}

bool CPlayer::ObjectInScanningRange(TUniqueId id, const CStateManager& mgr) const {
  const CEntity* ent = mgr.GetObjectById(id);
  if (const TCastToConstPtr<CActor> act = ent) {
    const zeus::CVector3f delta = act->GetTranslation() - GetTranslation();
    if (delta.canBeNormalized()) {
      return delta.magnitude() < g_tweakPlayer->GetScanningRange();
    }
  }
  return false;
}

void CPlayer::SetPlayerHitWallDuringMove() {
  x9c5_29_hitWall = true;
  x2d0_curAcceleration = 1;
}

void CPlayer::Touch(CActor& actor, CStateManager& mgr) {
  if (x2f8_morphBallState != EPlayerMorphBallState::Morphed) {
    return;
  }

  x768_morphball->Touch(actor, mgr);
}

void CPlayer::CVisorSteam::SetSteam(float targetAlpha, float alphaInDur, float alphaOutDur, CAssetId txtr,
                                    bool affectsThermal) {
  if (!x1c_txtr.IsValid() || targetAlpha > x10_nextTargetAlpha) {
    x10_nextTargetAlpha = targetAlpha;
    x14_nextAlphaInDur = alphaInDur;
    x18_nextAlphaOutDur = alphaOutDur;
    x1c_txtr = txtr;
  }
  x28_affectsThermal = affectsThermal;
}

CAssetId CPlayer::CVisorSteam::GetTextureId() const { return xc_tex; }

void CPlayer::CVisorSteam::Update(float dt) {
  if (!x1c_txtr.IsValid()) {
    x0_curTargetAlpha = 0.f;
  } else {
    x0_curTargetAlpha = x10_nextTargetAlpha;
    x4_curAlphaInDur = x14_nextAlphaInDur;
    x8_curAlphaOutDur = x18_nextAlphaOutDur;
    xc_tex = x1c_txtr;
  }

  x1c_txtr.Reset();
  if (zeus::close_enough(x20_alpha, x0_curTargetAlpha) && zeus::close_enough(x20_alpha, 0.f)) {
    return;
  }

  if (x20_alpha > x0_curTargetAlpha) {
    if (x24_delayTimer <= 0.f) {
      x20_alpha -= dt / x8_curAlphaOutDur;
      if (x20_alpha < x0_curTargetAlpha) {
        x20_alpha = x0_curTargetAlpha;
      }
    } else {
      x24_delayTimer -= dt;
      if (x24_delayTimer < 0.f) {
        x24_delayTimer = 0.f;
      }
    }
    return;
  }

  const CToken tmpTex = g_SimplePool->GetObj({SBIG('TXTR'), xc_tex});
  if (!tmpTex) {
    return;
  }

  x20_alpha += dt / x4_curAlphaInDur;
  if (x20_alpha > x0_curTargetAlpha) {
    x20_alpha = x0_curTargetAlpha;
  }

  x24_delayTimer = 0.1f;
}

void CPlayer::CFailsafeTest::Reset() {
  x0_stateSamples.clear();
  x54_posSamples.clear();
  x148_velSamples.clear();
  x23c_inputSamples.clear();
}

void CPlayer::CFailsafeTest::AddSample(EInputState state, const zeus::CVector3f& pos, const zeus::CVector3f& vel,
                                       const zeus::CVector2f& input) {
  if (x0_stateSamples.size() >= 20) {
    x0_stateSamples.resize(19);
  }
  x0_stateSamples.insert(x0_stateSamples.begin(), state);

  if (x54_posSamples.size() >= 20) {
    x54_posSamples.resize(19);
  }
  x54_posSamples.insert(x54_posSamples.begin(), pos);

  if (x148_velSamples.size() >= 20) {
    x148_velSamples.resize(19);
  }
  x148_velSamples.insert(x148_velSamples.begin(), vel);

  if (x23c_inputSamples.size() >= 20) {
    x23c_inputSamples.resize(19);
  }
  x23c_inputSamples.insert(x23c_inputSamples.begin(), input);
}

bool CPlayer::CFailsafeTest::Passes() const {
  if (x0_stateSamples.size() != 20) {
    return false;
  }

  float posMag = 0.f;

  zeus::CAABox velAABB(x148_velSamples[0], x148_velSamples[0]);
  zeus::CAABox posAABB(x54_posSamples[0], x54_posSamples[0]);
  zeus::CVector3f inputVec(x23c_inputSamples[0].x(), x23c_inputSamples[0].y(), 0.f);
  zeus::CAABox inputAABB(inputVec, inputVec);

  float maxVelMag = x148_velSamples[0].magnitude();
  float minVelMag = maxVelMag;
  u32 notEqualStates = 0;

  for (int i = 1; i < 20; ++i) {
    const float mag = (x54_posSamples[i - 1] - x54_posSamples[i]).magSquared();
    if (mag > FLT_EPSILON) {
      posMag += std::sqrt(mag);
    }

    posAABB.accumulateBounds(x54_posSamples[i]);
    velAABB.accumulateBounds(x148_velSamples[i]);
    const float velMag = x148_velSamples[i].magnitude();
    minVelMag = std::min(minVelMag, velMag);
    maxVelMag = std::max(maxVelMag, velMag);

    const zeus::CVector3f inputVec2(x23c_inputSamples[i].x(), x23c_inputSamples[i].y(), 0.f);
    inputAABB.accumulateBounds(inputVec2);

    if (x0_stateSamples[i] != x0_stateSamples[i - 1]) {
      notEqualStates += 1;
    }
  }

  bool test1 = true;
  if (posMag >= 1.f / 30.f && posMag >= minVelMag / 30.f) {
    test1 = false;
  }

  if (notEqualStates == 0 && x0_stateSamples[0] == EInputState::StartingJump) {
    const float inputMag = (inputAABB.max - inputAABB.min).magnitude();
    zeus::CAABox inputFrom0AABB(inputAABB);
    inputFrom0AABB.accumulateBounds(zeus::skZero3f);
    bool test2 = true;
    if ((inputFrom0AABB.max - inputFrom0AABB.min).magnitude() >= 0.01f && inputMag <= 1.5f) {
      test2 = false;
    }
    return test1 && test2;
  }

  return false;
}

void CPlayer::SetSpawnedMorphBallState(EPlayerMorphBallState state, CStateManager& mgr) {
  x2fc_spawnedMorphBallState = state;
  SetCameraState(EPlayerCameraState::Spawned, mgr);
  if (x2fc_spawnedMorphBallState != x2f8_morphBallState) {
    CPhysicsActor::Stop();
    SetOrbitRequest(EPlayerOrbitRequest::Respawn, mgr);
    switch (x2fc_spawnedMorphBallState) {
    case EPlayerMorphBallState::Unmorphed: {
      zeus::CVector3f pos;
      if (CanLeaveMorphBallState(mgr, pos)) {
        SetTranslation(GetTranslation() + pos);
        LeaveMorphBallState(mgr);
        ForceGunOrientation(x34_transform, mgr);
        DrawGun(mgr);
      }
      break;
    }
    case EPlayerMorphBallState::Morphed:
      EnterMorphBallState(mgr);
      ActivateMorphBallCamera(mgr);
      mgr.GetCameraManager()->SetupBallCamera(mgr);
      mgr.GetCameraManager()->GetBallCamera()->Reset(CreateTransformFromMovementDirection(), mgr);
      break;
    default:
      break;
    }
  }
}

void CPlayer::DecrementEnvironmentDamage() {
  if (xa10_envDmgCounter == 0) {
    return;
  }

  xa10_envDmgCounter--;
}

void CPlayer::IncrementEnvironmentDamage() {
  if (xa10_envDmgCounter != 0) {
    xa10_envDmgCounter++;
  } else {
    xa14_envDmgCameraShakeTimer = 0.f;
  }
}

bool CPlayer::CheckSubmerged() const {
  if (xe6_24_fluidCounter == 0) {
    return false;
  }

  return x828_distanceUnderWater >= (x2f8_morphBallState == EPlayerMorphBallState::Morphed
                                         ? 2.f * g_tweakPlayer->GetPlayerBallHalfExtent()
                                         : 0.5f * GetEyeHeight());
}

void CPlayer::UpdateSubmerged(CStateManager& mgr) {
  x82c_inLava = false;
  x828_distanceUnderWater = 0.f;

  if (xe6_24_fluidCounter == 0) {
    return;
  }

  if (const TCastToConstPtr<CScriptWater> water = mgr.ObjectById(xc4_fluidId)) {
    x828_distanceUnderWater = -(zeus::skUp.dot(x34_transform.origin) - water->GetTriggerBoundsWR().max.z());
    const EFluidType fluidType = water->GetFluidPlane().GetFluidType();
    x82c_inLava = (fluidType == EFluidType::Lava || fluidType == EFluidType::ThickLava);
    CheckSubmerged();
  }
}

void CPlayer::ApplySubmergedPitchBend(CSfxHandle& sfx) {
  if (!CheckSubmerged()) {
    return;
  }

  CSfxManager::PitchBend(sfx, -1.f);
}

void CPlayer::DetachActorFromPlayer() {
  x26c_attachedActor = kInvalidUniqueId;
  x270_attachedActorTime = 0.f;
  xa28_attachedActorStruggle = 0.f;
  x490_gun->SetActorAttached(false);
}

bool CPlayer::AttachActorToPlayer(TUniqueId id, bool disableGun) {
  if (x26c_attachedActor != kInvalidUniqueId) {
    return false;
  }

  if (disableGun) {
    x490_gun->SetActorAttached(true);
  }

  x26c_attachedActor = id;
  x270_attachedActorTime = 0.f;
  xa28_attachedActorStruggle = 0.f;
  x768_morphball->StopEffects();
  return true;
}

float CPlayer::GetAverageSpeed() const {
  if (const auto avg = x4a4_moveSpeedAvg.GetAverage()) {
    return *avg;
  }
  return x4f8_moveSpeed;
}

} // namespace metaforce
