// Fill out your copyright notice in the Description page of Project Settings.


#include "FpsPlayer.h"

#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.h"
#include "Camera/CameraComponent.h"
#include "Camera/CameraTypes.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "InputActionValue.h"
#include "CharacterAppearanceComponent.h"
#include "CraftingComponent.h"
#include "FootstepComponent.h"
#include "FpsPlayerController.h"
#include "HeadBobComponent.h"
#include "HeldItemComponent.h"
#include "InteractionComponent.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "Net/UnrealNetwork.h"
#include "PickupItem.h"
#include "SurvivalGameInstance.h"
#include "SurvivalPlayerState.h"
#include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h"

float FCameraSpring::Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset)
{
	// L'impulsion n'est pas versee d'un coup : la vitesse passerait de 0 a
	// plusieurs centaines d'unites par seconde en une frame, et cette
	// discontinuite se lit comme un a-coup. On l'etale sur RampTime.
	if (!FMath::IsNearlyZero(PendingImpulse))
	{
		const float ReleaseRatio = (RampTime > KINDA_SMALL_NUMBER)
			? FMath::Clamp(Step / RampTime, 0.f, 1.f)
			: 1.f;
		const float Released = PendingImpulse * ReleaseRatio;
		Velocity += Released;
		PendingImpulse -= Released;
	}

	// Ressort amorti classique : acceleration = -k*x - c*v.
	// On exprime k et c a partir d'une frequence et d'un taux d'amortissement,
	// beaucoup plus parlants a regler qu'une raideur brute.
	const float Omega = 2.f * PI * Frequency;
	const float Acceleration = -(Omega * Omega * Offset) - (2.f * Damping * Omega * Velocity);
	Velocity += Acceleration * Step;
	Offset = FMath::Clamp(Offset + Velocity * Step, -MaxOffset, MaxOffset);

	return Offset;
}

// Sets default values
AFpsPlayer::AFpsPlayer()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	// Gabarit du joueur : ~176 cm de haut, 34 cm de rayon
	UCapsuleComponent* Capsule = GetCapsuleComponent();
	Capsule->InitCapsuleSize(34.f, 88.f);

	// La souris fait tourner le personnage horizontalement (yaw) ;
	// le pitch reste sur le controller et n'est applique qu'a la camera.
	bUseControllerRotationYaw = true;
	bUseControllerRotationPitch = false;
	bUseControllerRotationRoll = false;

	// Camera a hauteur des yeux. La position est exprimee par rapport au centre
	// de la capsule, d'ou la soustraction de la demi-hauteur.
	FirstPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
	FirstPersonCamera->SetupAttachment(Capsule);
	FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightStanding - Capsule->GetUnscaledCapsuleHalfHeight()));
	FirstPersonCamera->SetFieldOfView(BaseFOV);
	// On pilote la rotation nous-memes dans UpdateCameraEffects() : bUsePawnControlRotation
	// ecraserait la rotation chaque frame et tous les effets de tilt seraient invisibles.
	FirstPersonCamera->bUsePawnControlRotation = false;

	// Bras vue premiere personne, accroches a la camera pour suivre le regard.
	FirstPersonArms = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonArms"));
	FirstPersonArms->SetupAttachment(FirstPersonCamera);
	// Symetrique exact du mesh 3e personne ci-dessous : chacun ne voit que ses
	// propres bras. Sans ca, les bras d'un ami flotteraient devant notre objectif.
	FirstPersonArms->SetOnlyOwnerSee(true);
	// Un mesh colle a l'objectif projetterait une ombre enorme en travers du
	// decor. L'ombre du joueur, elle, vient du mesh 3e personne.
	FirstPersonArms->SetCastShadow(false);
	FirstPersonArms->bCastDynamicShadow = false;
	// Les bras ne sont pas dans le monde, ils sont devant l'objectif : une
	// collision les ferait pousser des objets et surtout bloquer le trace
	// d'interaction, qui part exactement d'entre eux.
	FirstPersonArms->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	// Chez l'hote, le pawn de chaque ami porte AUSSI ce composant, invisible pour
	// tout le monde. Sans cette option le serveur evaluerait quatre AnimBlueprints
	// pour quatre meshes que personne ne rend.
	FirstPersonArms->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered;
	// Point de depart a affiner dans BP_FpsPlayer : la bonne valeur depend de
	// l'origine du FBX (racine aux pieds ? au bassin ?) et de l'orientation
	// d'export. Le yaw de -90 est le decalage habituel d'un FBX Blender/Maya.
	FirstPersonArms->SetRelativeLocationAndRotation(FVector(0.f, 0.f, -160.f), FRotator(0.f, -90.f, 0.f));

	HeadBob = CreateDefaultSubobject<UHeadBobComponent>(TEXT("HeadBob"));

	InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
	InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
	CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
	SurvivalStats = CreateDefaultSubobject<USurvivalStatsComponent>(TEXT("SurvivalStats"));
	AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
	HeldItemComponent = CreateDefaultSubobject<UHeldItemComponent>(TEXT("HeldItemComponent"));
	FootstepComponent = CreateDefaultSubobject<UFootstepComponent>(TEXT("FootstepComponent"));

	// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
	// mais on garde son ombre.
	GetMesh()->SetOwnerNoSee(true);
	GetMesh()->SetCastHiddenShadow(true);

	UCharacterMovementComponent* Movement = GetCharacterMovement();
	Movement->MaxWalkSpeed = WalkSpeed;
	Movement->MaxWalkSpeedCrouched = CrouchSpeed;
	Movement->JumpZVelocity = 450.f;
	Movement->AirControl = 0.3f;
	Movement->BrakingDecelerationWalking = 2000.f;
	// 120 cm de haut accroupi (une vraie position accroupie, pas un rampant)
	Movement->SetCrouchedHalfHeight(60.f);
	// Indispensable, sinon Crouch() est ignore
	Movement->GetNavAgentPropertiesRef().bCanCrouch = true;
}

// Called when the game starts or when spawned
void AFpsPlayer::BeginPlay()
{
	Super::BeginPlay();

	SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath);

	// Chez l'hote et en solo, le PlayerState est deja la a cet instant :
	// OnRep_PlayerState ne s'y declenchant jamais, c'est ce seul appel qui monte
	// le personnage. Chez un client il rend souvent la main sans rien faire,
	// l'OnRep prenant le relais -- les deux chemins sont idempotents.
	RefreshAppearanceFromPlayerState();

	// Sans cet abonnement, un objet fabrique alors que l'inventaire est plein
	// serait purement et simplement perdu.
	CraftingComponent->OnCraftOverflow.AddDynamic(this, &AFpsPlayer::HandleCraftOverflow);

	// Les reglages d'abord : le pawn est recree a chaque respawn et repart des
	// valeurs de son CDO, il doit donc les relire lui-meme. L'abonnement couvre
	// ensuite les changements faits en cours de partie depuis le menu de pause.
	ApplyUserSettings();

	if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
	{
		Settings->OnSurvivalSettingsApplied.AddDynamic(this, &AFpsPlayer::ApplyUserSettings);
	}

	// Le FOV est interpole chaque frame vers BaseFOV : sans ce calage initial on
	// verrait la camera s'ouvrir lentement jusqu'a la valeur reglee a chaque
	// apparition.
	FirstPersonCamera->SetFieldOfView(BaseFOV);

	// On enregistre le mapping context aupres du sous-systeme Enhanced Input
	// du joueur local. Sans ca, aucune touche n'est lue.
	if (const APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
			ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			if (DefaultMappingContext)
			{
				// La DECLARATION des contextes remappables appartient au
				// GameInstance -- le menu principal n'a pas de pawn et doit
				// pourtant afficher les touches. On la demande quand meme ici, et
				// AVANT d'activer le contexte : c'est elle qui traduit les touches
				// vers la disposition clavier du joueur, et un AZERTY doit trouver
				// ses touches des la premiere image, pas au premier passage dans
				// l'ecran de reglages. L'appel est idempotent.
				if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
				{
					GameInstance->RegisterMappableContexts();
				}

				Subsystem->AddMappingContext(DefaultMappingContext, 0);
			}
			else
			{
				UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DefaultMappingContext n'est pas assigne, aucun input ne fonctionnera."));
			}
		}
	}
}

void AFpsPlayer::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	// Le singleton de reglages survit au monde : sans ce desabonnement, chaque
	// cadavre laisserait derriere lui un abonne mort de plus.
	if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
	{
		Settings->OnSurvivalSettingsApplied.RemoveDynamic(this, &AFpsPlayer::ApplyUserSettings);
	}

	Super::EndPlay(EndPlayReason);
}

void AFpsPlayer::ApplyUserSettings()
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings)
	{
		// Pas de reglages : on garde les valeurs du Blueprint. Le jeu reste
		// jouable, il n'est simplement pas configurable.
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : USurvivalUserSettings introuvable, les valeurs du Blueprint sont conservees."));
		return;
	}

	LookSensitivityX = Settings->GetLookMultiplierX();
	LookSensitivityY = Settings->GetLookMultiplierY();
	bInvertLookY = Settings->GetInvertLookY();
	BaseFOV = Settings->GetFieldOfView();
	CameraShakeMultiplier = Settings->GetCameraShakeMultiplier();
	bSprintToggleMode = Settings->GetSprintToggle();
	bCrouchToggleMode = Settings->GetCrouchToggle();

	// Le curseur "secousses de camera" vaut aussi pour le balancement : un joueur
	// qui le met a zero demande une camera fixe, pas une camera fixe qui respire.
	HeadBob->SetIntensityScale(CameraShakeMultiplier);

	// Repasser en maintien alors que la touche n'est plus enfoncee laisserait le
	// personnage bloque en sprint : on repart d'un etat propre.
	if (!bSprintToggleMode && bIsSprinting)
	{
		StopSprint();
	}
}

// Called every frame
void AFpsPlayer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// On memorise la vitesse de chute a chaque frame : au moment ou Landed()
	// est appele, la vitesse a deja pu etre remise a zero par le moteur.
	if (GetCharacterMovement()->IsFalling())
	{
		LastFallSpeed = GetCharacterMovement()->Velocity.Z;

		// Et on efface la reception precedente des la premiere image de vol :
		// c'est ce qui permet aux transitions de la machine a etats de se
		// contenter de comparer l'enum, sans verifier bIsFalling par-dessus.
		LandingImpact = ELandingImpact::None;
	}

	UpdateCameraEffects(DeltaTime);
}

void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
{
	// Un gros hoquet de framerate (chargement, breakpoint) ferait diverger
	// l'integration du ressort ci-dessous. On plafonne le pas de temps.
	const float Step = FMath::Min(DeltaTime, 1.f / 30.f);

	// ==================================================================
	// 1. Animation d'accroupissement
	// ==================================================================
	// CrouchAlpha va de 0 (debout) a 1 (accroupi) en CrouchTransitionDuration
	// secondes. Duree fixe plutot qu'un FInterpTo : le mouvement est identique
	// a chaque fois et se termine vraiment, au lieu de tendre asymptotiquement.
	if (!FMath::IsNearlyEqual(CrouchAlpha, CrouchTargetAlpha))
	{
		const float CrouchStep = Step / FMath::Max(CrouchTransitionDuration, KINDA_SMALL_NUMBER);
		CrouchAlpha = FMath::Clamp(CrouchAlpha + CrouchStep * FMath::Sign(CrouchTargetAlpha - CrouchAlpha), 0.f, 1.f);
	}

	// ==================================================================
	// 2. Ressorts : impact (saut, atterrissage) et bascule de tete
	// ==================================================================
	PunchSpring.Update(Step, PunchFrequency, PunchDamping, PunchImpulseRampTime, PunchMaxOffset);

	// Le garde-fou est en unites normalisees, ou 1 = le pic d'une bascule
	// complete. A 1.5 on laisse passer un leger cumul quand deux impulsions se
	// suivent de tres pres, sans jamais laisser la tete partir.
	CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f);

	// ==================================================================
	// 3. Balancement de tete
	// ==================================================================
	// Integre ici et applique dans ApplyCameraTransform, exactement comme les
	// ressorts ci-dessus : c'est ce qui permet a CalcCamera de recomposer la
	// transform sans faire avancer le temps deux fois.
	HeadBob->UpdateBob(Step);

	// ==================================================================
	// 4. FOV de sprint
	// ==================================================================
	// On exige un vrai deplacement : sinon le FOV s'ouvrirait juste en
	// maintenant Shift a l'arret, ce qui donne un effet de pompage bizarre.
	const bool bMovingFastEnough = GetCharacterMovement()->Velocity.SizeSquared2D() > FMath::Square(WalkSpeed * 0.9f);
	const float TargetFOV = BaseFOV + ((bIsSprinting && bMovingFastEnough) ? SprintFOVOffset : 0.f);
	FirstPersonCamera->SetFieldOfView(FMath::FInterpTo(FirstPersonCamera->FieldOfView, TargetFOV, Step, FOVInterpSpeed));

	// L'application est laissee a ApplyCameraTransform : CalcCamera la rappelle
	// juste avant que la vue soit lue, avec l'etat le plus frais de la capsule.
	ApplyCameraTransform();
}

void AFpsPlayer::ApplyCameraTransform()
{
	// ==================================================================
	// Position
	// ==================================================================
	// SmoothStep donne une courbe en S : demarrage doux, acceleration, arrivee douce.
	const float Smoothed = FMath::SmoothStep(0.f, 1.f, CrouchAlpha);
	const float EyeHeightAboveFeet = FMath::Lerp(EyeHeightStanding, EyeHeightCrouched, Smoothed);

	// Point cle : quand la capsule retrecit, le CharacterMovement redescend l'acteur
	// d'un coup pour garder les pieds au sol. La camera, attachee a la capsule,
	// suivait ce saut instantane -- c'est ce qui rendait le crouch "snap".
	// En raisonnant en hauteur d'oeil AU-DESSUS DES PIEDS et en soustrayant la
	// demi-hauteur courante de la capsule, ce saut est annule exactement --
	// a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera.
	const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();

	// Le balancement s'AJOUTE au reste. Son Y est un decalage lateral dans le
	// repere de la capsule, dont le yaw suit deja le regard (bUseControllerRotationYaw) :
	// la tete se porte donc bien a droite de ce qu'on regarde, sans conversion.
	// Le tangage, lui, n'est pas dans la capsule -- c'est voulu, le balancement
	// reste horizontal meme quand on regarde ses pieds.
	const FVector BobOffset = HeadBob->GetLocationOffset();

	FirstPersonCamera->SetRelativeLocation(FVector(
		BobOffset.X,
		BobOffset.Y,
		EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset + BobOffset.Z));

	// ==================================================================
	// Rotation
	// ==================================================================
	// La bascule ne depend plus de l'avancement de la transition : c'est l'etat
	// d'un ressort, donc elle reste continue meme si le joueur change d'avis en
	// plein mouvement. L'ancienne forme sin(alpha*PI) multipliee par un signe
	// sautait de +CrouchTiltPitch a -CrouchTiltPitch en une frame des qu'on
	// s'accroupissait et se relevait coup sur coup.
	const float CrouchTilt = CrouchTiltSpring.Offset;

	FRotator ViewOffset(-CrouchTiltPitch * CrouchTilt, 0.f, CrouchTiltRoll * CrouchTilt);
	// Le regard suit le deplacement vertical de la camera : elle descend, on
	// regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage.
	ViewOffset.Pitch += PunchSpring.Offset * PunchPitchPerCm;

	// Roulis et tangage du balancement, dans le meme sac que le reste.
	ViewOffset += HeadBob->GetRotationOffset();

	if (const AController* OwningController = GetController())
	{
		FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset);
	}
}

void AFpsPlayer::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult)
{
	ApplyCameraTransform();

	Super::CalcCamera(DeltaTime, OutResult);
}

void AFpsPlayer::AddCrouchTilt(float Direction)
{
	// Impulsion normalisee pour qu'un ressort critique culmine exactement a 1 :
	// x(t) = v0 * t * e^(-w*t) atteint son maximum v0/(e*w) en t = 1/w, donc
	// v0 = e*w. CrouchTiltPitch et CrouchTiltRoll restent ainsi des degres au
	// pic, et non un reglage a l'aveugle.
	const float Omega = 2.f * PI * CrouchTiltFrequency;
	CrouchTiltSpring.AddImpulse(Direction * UE_EULERS_NUMBER * Omega);
}

void AFpsPlayer::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	RefreshAppearanceFromPlayerState();
}

void AFpsPlayer::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	// Cote serveur uniquement : chez l'hote c'est ce chemin qui remplit sa
	// propre main, IsLocallyControlled() ne devenant vrai qu'ici.
	if (HeldItemComponent)
	{
		HeldItemComponent->RefreshFromInventory();
	}
}

void AFpsPlayer::OnRep_Controller()
{
	Super::OnRep_Controller();

	// Le pendant client. Meme raison qu'OnRep_PlayerState juste au-dessus : rien
	// ne dit lequel du pawn ou du controller arrive le premier, donc les deux
	// bouts de la course appellent la meme fonction idempotente.
	if (HeldItemComponent)
	{
		HeldItemComponent->RefreshFromInventory();
	}
}

void AFpsPlayer::RefreshAppearanceFromPlayerState()
{
	if (!AppearanceComponent)
	{
		return;
	}

	// Absent n'est pas une erreur : c'est le cas normal quand le pawn gagne la
	// course. L'apparence viendra par OnRep_CharacterAppearance.
	if (const ASurvivalPlayerState* State = GetPlayerState<ASurvivalPlayerState>())
	{
		AppearanceComponent->ApplyAppearance(State->GetCharacterAppearance());
	}
}

// ----------------------------------------------------------------------
// Commandes console d'apparence -- de simples delegations. Voir le commentaire
// de FpsPlayer.h : les UFUNCTION(Exec) d'un composant ne sont jamais routees,
// le pawn est le maillon le plus proche qui le soit.
// ----------------------------------------------------------------------

void AFpsPlayer::EmberPart(const FString& Part, int32 Index)
{
	if (AppearanceComponent)
	{
		AppearanceComponent->ApplyPartByName(Part, Index);
		SubmitCurrentAppearance();
	}
}

void AFpsPlayer::EmberColor(const FString& Part, int32 ColorIndex)
{
	if (AppearanceComponent)
	{
		AppearanceComponent->ApplyColorByName(Part, ColorIndex);
		SubmitCurrentAppearance();
	}
}

void AFpsPlayer::SubmitCurrentAppearance()
{
	// Applique en local D'ABORD (c'est deja fait par l'appelant), soumis
	// ensuite : le contraire ferait attendre l'aller-retour reseau pour voir
	// son propre changement, ce qui donnerait un reglage qui repond en retard.
	// C'est le meme ordre que Server_SetSprinting.
	if (AFpsPlayerController* FpsController = Cast<AFpsPlayerController>(GetController()))
	{
		FpsController->SubmitCharacterAppearance(AppearanceComponent->GetAppearance());
	}
}

void AFpsPlayer::EmberAppearanceDump()
{
	if (AppearanceComponent)
	{
		AppearanceComponent->LogAppearance();
	}
}

void AFpsPlayer::AddCameraPunch(float Strength)
{
	// Le reglage s'applique ICI et pas sur chaque source d'impact : tout ce qui
	// secoue la camera passe par cette porte, y compris les explosions et les
	// reculs d'arme a venir. A 0, la camera ne bouge plus du tout.
	// Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects
	// qui l'injecte progressivement dans le ressort.
	PunchSpring.AddImpulse(Strength * CameraShakeMultiplier);
}

void AFpsPlayer::Landed(const FHitResult& Hit)
{
	Super::Landed(Hit);

	// L'amplitude suit la vitesse de chute : une marche d'escalier ne doit pas
	// secouer autant qu'une chute de dix metres.
	const float FallSpeed = FMath::Abs(LastFallSpeed);
	if (FallSpeed > LandPunchMinFallSpeed)
	{
		const float Range = FMath::Max(LandPunchMaxFallSpeed - LandPunchMinFallSpeed, 1.f);
		const float Ratio = FMath::Clamp((FallSpeed - LandPunchMinFallSpeed) / Range, 0.f, 1.f);
		AddCameraPunch(-LandPunchStrength * Ratio);

		// Aide au reglage : montre la vitesse d'impact reelle et l'amplitude
		// obtenue. Supprime ce log quand le ressenti te convient.
		UE_LOG(LogTemp, Log, TEXT("Atterrissage : chute %.0f cm/s, ratio %.2f, impulsion %.0f cm/s (~%.1f cm de camera)"),
			FallSpeed, Ratio, LandPunchStrength * Ratio, LandPunchStrength * Ratio * 0.018f);
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Atterrissage ignore : chute %.0f cm/s, sous le seuil de %.0f cm/s"),
			FallSpeed, LandPunchMinFallSpeed);
	}

	// Les degats decident de tout : c'est la meme valeur qui fait perdre de la
	// vie et qui fait encaisser les bras. Une reception lourde est, par
	// definition, une chute qui a fait mal.
	const float Damage = ComputeFallDamage(FallSpeed);

	// Pas de garde d'autorite ici : USurvivalStatsComponent::ApplyDamage la pose
	// deja, et la dupliquer donnerait deux endroits ou la regle peut changer.
	// Chez un client l'appel ne fait donc rien -- sa vie descend par OnRep_Stats.
	if (Damage > 0.f && SurvivalStats)
	{
		SurvivalStats->ApplyDamage(Damage);
	}

	// Le geste des bras suit le meme seuil bas que la camera : une marche
	// d'escalier ne doit ni secouer la vue ni faire encaisser le personnage.
	// Sous ce seuil l'enum reste a None, et la machine a etats repart droit de
	// la chute vers la locomotion sans jouer de reception.
	if (FallSpeed <= LandPunchMinFallSpeed)
	{
		LandingImpact = ELandingImpact::None;
	}
	else
	{
		LandingImpact = (Damage > 0.f) ? ELandingImpact::Hard : ELandingImpact::Soft;
	}

	LastFallSpeed = 0.f;
}

float AFpsPlayer::ComputeFallDamage(float FallSpeed) const
{
	// Comparaison STRICTE : a vitesse nulle comme a toute vitesse sous le seuil,
	// la chute ne coute rien. Avec un >=, un FallDamageMinSpeed mis a zero en
	// PIE ferait perdre de la vie a chaque pas.
	if (FallSpeed <= FallDamageMinSpeed)
	{
		return 0.f;
	}

	// Lineaire en VITESSE et pas en energie : la vitesse est ce que le joueur
	// percoit -- le temps qu'il passe a tomber -- alors qu'un modele en v²
	// rendrait les dix derniers metres brutalement mortels sans que rien a
	// l'ecran ne l'ait annonce.
	return FMath::GetMappedRangeValueClamped(
		FVector2D(FallDamageMinSpeed, FMath::Max(FallDamageMaxSpeed, FallDamageMinSpeed + 1.f)),
		FVector2D(FallDamageMin, FallDamageMax),
		FallSpeed);
}

void AFpsPlayer::OnJumped_Implementation()
{
	Super::OnJumped_Implementation();

	AddCameraPunch(-JumpPunchStrength);
}

// Called to bind functionality to input
void AFpsPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
	if (!Input)
	{
		UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : l'input component n'est pas un UEnhancedInputComponent."));
		return;
	}

	if (MoveAction)
	{
		Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AFpsPlayer::Move);
	}

	if (LookAction)
	{
		Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AFpsPlayer::Look);
	}

	if (JumpAction)
	{
		// Jump() et StopJumping() viennent directement de ACharacter
		Input->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
		Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
	}

	if (SprintAction)
	{
		// Le mode maintien / bascule se decide dans les handlers et non ici :
		// rebrancher les bindings a chaud demanderait de reconstruire l'input
		// component a chaque changement de reglage.
		Input->BindAction(SprintAction, ETriggerEvent::Started, this, &AFpsPlayer::StartSprint);
		Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleSprintReleased);
	}

	if (CrouchAction)
	{
		Input->BindAction(CrouchAction, ETriggerEvent::Started, this, &AFpsPlayer::StartCrouch);
		Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleCrouchReleased);
	}

	if (InteractAction)
	{
		Input->BindAction(InteractAction, ETriggerEvent::Started, this, &AFpsPlayer::Interact);
	}

	if (UseItemAction)
	{
		Input->BindAction(UseItemAction, ETriggerEvent::Started, this, &AFpsPlayer::UseSelectedItem);
	}
}

void AFpsPlayer::Move(const FInputActionValue& Value)
{
	const FVector2D Axis = Value.Get<FVector2D>();
	if (Axis.IsNearlyZero() || !Controller)
	{
		return;
	}

	// On se deplace par rapport a la direction du regard, mais a plat :
	// on ne garde que le yaw pour ne pas "voler" en regardant vers le haut.
	const FRotator YawRotation(0.f, Controller->GetControlRotation().Yaw, 0.f);
	const FVector Forward = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	const FVector Right = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

	AddMovementInput(Forward, Axis.Y);
	AddMovementInput(Right, Axis.X);
}

void AFpsPlayer::Look(const FInputActionValue& Value)
{
	// Si IA_Look n'est pas en Axis2D, Get<FVector2D>() renvoie toujours Y = 0
	// et le regard reste bloque sur l'axe horizontal. On le signale une fois.
	if (Value.GetValueType() != EInputActionValueType::Axis2D && !bLoggedLookTypeWarning)
	{
		bLoggedLookTypeWarning = true;
		UE_LOG(LogTemp, Error,
			TEXT("AFpsPlayer : IA_Look doit avoir Value Type = Axis2D (Vector2D). ")
			TEXT("Avec un autre type, l'axe vertical est ignore."));
	}

	const FVector2D Axis = Value.Get<FVector2D>();

	// Pas de DeltaTime ici : la souris fournit un delta, pas une vitesse.
	// Le multiplier par DeltaTime rendrait la visee dependante du framerate.
	AddControllerYawInput(Axis.X * LookSensitivityX);
	AddControllerPitchInput(Axis.Y * LookSensitivityY * (bInvertLookY ? -1.f : 1.f));
}

void AFpsPlayer::StartSprint()
{
	// Pas de sprint accroupi : on se releve d'abord
	if (bIsCrouched)
	{
		return;
	}

	// En mode bascule, la meme touche coupe le sprint.
	if (bSprintToggleMode && bIsSprinting)
	{
		StopSprint();
		return;
	}

	SetSprinting(true);
}

void AFpsPlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	// COND_SkipOwner : celui qui sprinte le sait deja, il vient de l'appliquer
	// en local. Le renvoyer ecraserait son etat courant par une valeur qui a un
	// aller-retour de retard -- exactement ce qui fait "sauter" un sprint qu'on
	// relance vite.
	DOREPLIFETIME_CONDITION(AFpsPlayer, bIsSprinting, COND_SkipOwner);
}

void AFpsPlayer::StopSprint()
{
	SetSprinting(false);
}

void AFpsPlayer::SetSprinting(bool bNewSprinting)
{
	if (bIsSprinting == bNewSprinting)
	{
		return;
	}

	bIsSprinting = bNewSprinting;
	ApplySprintSpeed();

	// Applique en local d'abord pour que la vitesse soit instantanee sous les
	// doigts, puis previent le serveur. Le sens inverse -- attendre la reponse --
	// donnerait un sprint qui demarre avec un temps de retard.
	if (!HasAuthority())
	{
		Server_SetSprinting(bNewSprinting);
	}
}

void AFpsPlayer::Server_SetSprinting_Implementation(bool bNewSprinting)
{
	bIsSprinting = bNewSprinting;
	ApplySprintSpeed();
}

void AFpsPlayer::OnRep_IsSprinting()
{
	ApplySprintSpeed();
}

void AFpsPlayer::ApplySprintSpeed()
{
	GetCharacterMovement()->MaxWalkSpeed = bIsSprinting ? SprintSpeed : WalkSpeed;
}

void AFpsPlayer::HandleSprintReleased()
{
	// En bascule, relacher la touche ne doit rien faire.
	if (bSprintToggleMode)
	{
		return;
	}

	StopSprint();
}

void AFpsPlayer::UseSelectedItem()
{
	// La case est figee ICI, a l'appui, et relue a l'impact : voir
	// PendingUseSlotIndex.
	PendingUseSlotIndex = InventoryComponent->GetSelectedHotbarIndex();

	// Le geste part AVANT le routage reseau, et sans rien attendre de lui : il
	// n'existe que sur cette machine (bras en OnlyOwnerSee) et doit repondre au
	// clic dans la frame. Le faire transiter par le serveur ajouterait un
	// aller-retour a la seule partie du systeme que le joueur ressent
	// immediatement.
	//
	// Consequence assumee : le geste se joue meme si le serveur refuse ensuite
	// l'action. Sur un coup de pioche c'est invisible ; pour un consommable qui
	// n'existe plus, c'est OnRep_Slots qui remet l'affichage d'aplomb.
	const EUseMontageResult Result = HeldItemComponent
		? HeldItemComponent->PlayUseMontage()
		: EUseMontageResult::NoMontage;

	switch (Result)
	{
	case EUseMontageResult::Started:
		// L'effet appartient desormais au montage : il partira de son marqueur,
		// au moment ou la hache touche. Rien a faire de plus a l'appui.
		return;

	case EUseMontageResult::AlreadyPlaying:
		// Marteler le bouton ne relance pas le bras ; il ne doit pas davantage
		// declencher l'effet, sinon un clic maintenu recolterait a chaque image
		// pendant que le geste, lui, n'en joue qu'un.
		return;

	default:
		break;
	}

	// Aucun montage a jouer : personne n'enverra de marqueur, l'effet part tout
	// de suite. C'est le comportement d'avant, conserve pour les objets sans
	// animation -- la plupart des consommables aujourd'hui.
	TriggerHeldItemAction();
}

void AFpsPlayer::TriggerHeldItemAction()
{
	// Le montage d'usage ne tourne aujourd'hui que sur les bras du joueur local,
	// donc cette garde ne rejette rien. Elle est la pour le jour ou un montage
	// sera joue sur le CORPS, visible des quatre machines : sans elle, chaque
	// coup de hache partirait en autant d'exemplaires qu'il y a de joueurs.
	if (!IsLocallyControlled() || !InventoryComponent)
	{
		return;
	}

	// L'hote agit tout de suite, le client demande. On envoie meme si le slot
	// parait vide chez nous : c'est le serveur qui fait foi, et son inventaire
	// peut avoir une frame d'avance sur le notre.
	if (!HasAuthority())
	{
		Server_UseItem(PendingUseSlotIndex);
		return;
	}

	PerformUseItem(PendingUseSlotIndex);
}

void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex)
{
	PerformUseItem(SlotIndex);
}

void AFpsPlayer::PerformUseItem(int32 SlotIndex)
{
	if (!HasAuthority() || !InventoryComponent->GetSlots().IsValidIndex(SlotIndex))
	{
		return;
	}

	const FInventorySlot Selected = InventoryComponent->GetSlots()[SlotIndex];
	if (Selected.IsEmpty())
	{
		return;
	}

	const UItemDataAsset* Item = Selected.Item;

	// Objet non consommable : on ne fait rien pour l'instant. C'est ici que
	// viendront les outils (frapper), les armes (tirer), les placeables.
	if (!Item->bIsConsumable)
	{
		return;
	}

	// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
	SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);

	// ConsumeUse gere les deux cas : usage unique (l'exemplaire disparait) et
	// usages multiples (une charge en moins, l'objet reste tant qu'il en a).
	// On vise le slot actif precisement, et pas RemoveItem qui piocherait
	// n'importe quelle pile du meme objet, y compris dans le sac.
	InventoryComponent->ConsumeUse(SlotIndex);
}

FVector AFpsPlayer::PlaceOnGround(const FVector& Start) const
{
	// APickupItem ne simule pas la physique : sans ce trace vers le bas,
	// l'objet resterait suspendu en l'air la ou on l'a lache.
	FCollisionQueryParams Params(SCENE_QUERY_STAT(DropGroundTrace), /*bTraceComplex=*/false, this);

	FHitResult GroundHit;
	if (GetWorld()->LineTraceSingleByChannel(GroundHit, Start, Start - FVector(0.f, 0.f, 500.f), ECC_Visibility, Params))
	{
		return GroundHit.ImpactPoint + FVector(0.f, 0.f, DropGroundOffset);
	}

	return Start;
}

APickupItem* AFpsPlayer::SpawnPickup(const FInventorySlot& Slot, const FVector& Location)
{
	if (!DroppedPickupClass || Slot.IsEmpty())
	{
		return nullptr;
	}

	// SpawnActorDeferred et non SpawnActor : il faut poser ItemData AVANT que
	// OnConstruction et BeginPlay tournent, sinon le pickup apparait sans mesh
	// et son CanInteract le rendrait inerte.
	const FTransform SpawnTransform(FRotator(0.f, GetActorRotation().Yaw, 0.f), Location);

	APickupItem* Dropped = GetWorld()->SpawnActorDeferred<APickupItem>(
		DroppedPickupClass, SpawnTransform, this, nullptr,
		ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

	if (!Dropped)
	{
		return nullptr;
	}

	Dropped->InitPickup(Slot.Item, Slot.Quantity, Slot.RemainingUses);
	Dropped->FinishSpawning(SpawnTransform);

	return Dropped;
}

bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity)
{
	return DropFromInventory(InventoryComponent, SlotIndex, Quantity);
}

void AFpsPlayer::Server_DropFromInventory_Implementation(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity)
{
	DropFromInventory(Inventory, SlotIndex, Quantity);
}

bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity)
{
	if (!Inventory)
	{
		return false;
	}

	// Jeter engendre un acteur dans le monde : c'est une action d'autorite.
	// La position de depot est calculee cote SERVEUR a partir de la camera
	// repliquee du pawn -- a quelques centimetres pres du point vise par le
	// client, ce qui n'a aucune importance pour un objet pose au sol.
	if (!HasAuthority())
	{
		Server_DropFromInventory(Inventory, SlotIndex, Quantity);
		return true;
	}

	if (!DroppedPickupClass)
	{
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, impossible de jeter un objet."));
		return false;
	}

	FInventorySlot Taken;
	if (!Inventory->TakeFromSlot(SlotIndex, Quantity, Taken) || Taken.IsEmpty())
	{
		return false;
	}

	const FVector EyeLocation = FirstPersonCamera->GetComponentLocation();
	const FVector Forward = FirstPersonCamera->GetForwardVector();
	FVector DropLocation = EyeLocation + Forward * DropDistance;

	// Un mur devant nous : on depose juste avant, sinon l'objet finirait
	// de l'autre cote de la cloison et deviendrait inatteignable.
	FCollisionQueryParams Params(SCENE_QUERY_STAT(DropItemTrace), /*bTraceComplex=*/false, this);
	FHitResult WallHit;
	if (GetWorld()->LineTraceSingleByChannel(WallHit, EyeLocation, DropLocation, ECC_Visibility, Params))
	{
		DropLocation = WallHit.ImpactPoint - Forward * 20.f;
	}

	if (!SpawnPickup(Taken, PlaceOnGround(DropLocation)))
	{
		// On ne perd pas l'objet : il retourne d'ou il vient, usure comprise.
		Inventory->AddItem(Taken.Item, Taken.Quantity, Taken.RemainingUses);
		return false;
	}

	return true;
}

void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
	if (!Item || Quantity <= 0)
	{
		return;
	}

	if (!DroppedPickupClass)
	{
		UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."),
			Quantity, *GetNameSafe(Item));
		return;
	}

	FInventorySlot Overflow;
	Overflow.Item = Item;
	Overflow.Quantity = Quantity;
	Overflow.RemainingUses = RemainingUses;

	// Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le
	// nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser
	// une cloison qu'on ne regarde meme pas.
	const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f;

	if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation)))
	{
		UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s issus de la fabrication."),
			Quantity, *GetNameSafe(Item));
	}
}

void AFpsPlayer::DropAllItems()
{
	// Declenche par la mort, qui sera arbitree par le serveur. La garde est la
	// pour le jour ou un client l'appellerait : il engendrerait des pickups
	// visibles de lui seul, qui disparaitraient au premier nettoyage reseau.
	if (!HasAuthority())
	{
		return;
	}

	if (!DroppedPickupClass)
	{
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, le butin est perdu."));
		return;
	}

	const int32 SlotCount = InventoryComponent->GetSlots().Num();
	const FVector Center = GetActorLocation();

	int32 Scattered = 0;
	for (int32 Index = 0; Index < SlotCount; ++Index)
	{
		FInventorySlot Taken;
		// TakeFromSlot renvoie false sur un slot vide : pas besoin de tester.
		if (!InventoryComponent->TakeFromSlot(Index, MAX_int32, Taken) || Taken.IsEmpty())
		{
			continue;
		}

		// Angle d'or : repartit les objets en spirale reguliere autour du
		// corps, sans qu'ils se superposent ni forment de motif visible.
		const float Angle = FMath::DegreesToRadians(Scattered * 137.5f);
		const float Radius = 40.f + Scattered * 4.f;
		const FVector Offset(FMath::Cos(Angle) * Radius, FMath::Sin(Angle) * Radius, 0.f);

		SpawnPickup(Taken, PlaceOnGround(Center + Offset));
		++Scattered;
	}

	UE_LOG(LogTemp, Log, TEXT("AFpsPlayer : %d piles laissees au sol a la mort."), Scattered);
}

void AFpsPlayer::HandleDeath()
{
	if (bIsDead)
	{
		return;
	}
	bIsDead = true;

	// Tout tombe sur place. On le fait AVANT de couper les inputs : l'ordre
	// n'a pas d'importance fonctionnelle, mais perdre le butin en silence
	// serait le pire bug possible dans un jeu de survie.
	DropAllItems();

	ClearTransientInputStates();
	GetCharacterMovement()->DisableMovement();

	// Un cadavre ne marche plus. Sans ca, la derniere foulee en cours produirait
	// encore son pas final au moment ou la vitesse tombe a zero -- et HandleDeath
	// tourne sur toutes les machines, donc les amis l'entendraient aussi.
	FootstepComponent->SetFootstepsEnabled(false);

	if (APlayerController* OwningController = Cast<APlayerController>(GetController()))
	{
		DisableInput(OwningController);
	}
}

void AFpsPlayer::ClearTransientInputStates()
{
	StopSprint();
	StopJumping();

	// L'accroupissement n'est volontairement pas annule : c'est un etat
	// visible que le joueur maitrise, et il se retablit tout seul au
	// prochain appui sur la touche.
}

void AFpsPlayer::StartCrouch()
{
	// En mode bascule, la meme touche releve le personnage. On teste l'INTENTION
	// (bWantsToCrouch) et non l'etat applique (bIsCrouched) : le CharacterMovement
	// ne redimensionne la capsule qu'a son propre tick, donc deux appuis dans la
	// meme frame verraient tous les deux bIsCrouched a false et le personnage
	// resterait accroupi. Symptome typique quand on martele la touche.
	if (bCrouchToggleMode && GetCharacterMovement()->bWantsToCrouch)
	{
		StopCrouch();
		return;
	}

	StopSprint();
	Crouch();
}

void AFpsPlayer::HandleCrouchReleased()
{
	if (bCrouchToggleMode)
	{
		return;
	}

	StopCrouch();
}

void AFpsPlayer::StopCrouch()
{
	// UnCrouch() echoue silencieusement s'il y a un plafond : le perso
	// reste accroupi jusqu'a ce que la place se libere.
	UnCrouch();
}

void AFpsPlayer::Interact()
{
	// Le geste part AVANT la RPC, pas apres sa reponse : un montage declenche a
	// l'aller-retour serveur donnerait des bras qui repondent 60 ms apres le clic,
	// et le joueur lirait ca comme un jeu qui rame. Le prix a payer est un geste
	// dans le vide si le serveur refuse l'interaction -- infiniment moins genant.
	//
	// Le test de type est ici et pas dans le composant d'interaction : celui-ci ne
	// connait que l'interface IInteractable et n'a pas a apprendre ce qu'est un
	// pickup. Un coffre ou un etabli n'ont pas a declencher un geste de ramassage.
	if (Cast<APickupItem>(InteractionComponent->GetFocusedActor()))
	{
		PlayArmsMontage(GrabMontage);
	}

	InteractionComponent->TryInteract();
}

float AFpsPlayer::PlayArmsMontage(UAnimMontage* Montage, float PlayRate)
{
	// IsLocallyControlled : chez l'hote, le pawn de chaque ami passerait aussi par
	// ici a chaque ramassage et animerait un mesh que personne ne rend.
	if (!Montage || !IsLocallyControlled())
	{
		return 0.f;
	}

	UAnimInstance* ArmsAnim = FirstPersonArms->GetAnimInstance();
	return ArmsAnim ? ArmsAnim->Montage_Play(Montage, PlayRate) : 0.f;
}

void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
	Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);

	CrouchTargetAlpha = 1.f;
	AddCrouchTilt(1.f);

	// Le geste des bras part d'ici et pas d'une transition d'AnimBP : c'est le
	// seul endroit qui soit certain que la capsule change VRAIMENT de taille.
	// Le CharacterMovement peut refuser un accroupissement, et une machine a
	// etats qui suivrait la touche jouerait alors un geste sans rien derriere.
	if (HeldItemComponent)
	{
		HeldItemComponent->PlayCrouchMontage(/*bEntering=*/true);
	}
}

void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
	Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);

	CrouchTargetAlpha = 0.f;
	AddCrouchTilt(-1.f);

	// Symetrique exact d'OnStartCrouch : se relever peut etre refuse par un
	// plafond bas, et c'est le moteur qui tranche, pas la touche.
	if (HeldItemComponent)
	{
		HeldItemComponent->PlayCrouchMontage(/*bEntering=*/false);
	}
}
