// Lot CAM-B2c - voir LPCameraModeSubsystem.h #include "LPCameraModeSubsystem.h" #include "Blueprint/UserWidget.h" #include "Blueprint/WidgetTree.h" #include "Components/Button.h" #include "Components/CanvasPanelSlot.h" #include "Components/HorizontalBoxSlot.h" #include "Components/Image.h" #include "Components/PanelWidget.h" #include "Components/TextBlock.h" #include "Engine/Texture2D.h" #include "Widgets/Layout/SBorder.h" #include "Engine/GameInstance.h" #include "Engine/GameViewportClient.h" #include "Engine/World.h" #include "Components/SkeletalMeshComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/OverlapResult.h" #include "Engine/StaticMesh.h" #include "Framework/Application/SlateApplication.h" #include "GameFramework/Actor.h" #include "GameFramework/Character.h" #include "GameFramework/InputSettings.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PlayerInput.h" #include "LPKeyboardSubsystem.h" #include "LPShowCoupleSubsystem.h" #include "LifePlay.h" #include "Styling/CoreStyle.h" #include "TimerManager.h" #include "UObject/UObjectIterator.h" #include "UObject/UnrealType.h" #include "Widgets/Input/SEditableText.h" #include "Widgets/SOverlay.h" #include "Widgets/Text/STextBlock.h" // Espace de noms nomme, pas anonyme : en compilation unitaire, ce fichier peut partager une unite avec // LPDirectControlSubsystem.cpp et LPOutdoorRoomSubsystem.cpp, dont l'espace anonyme a des noms voisins // (MenuClasses, MenuOnScreen, GetObjectProperty) : risque C2374 (lp-build). Appels toujours qualifies. namespace LPCameraModeLocal { const float TickInterval = 0.1f; const int32 ViewportZOrder = 50; // Variables de GI et de Room (noms exacts du paragraphe 2 de la SPEC). const FName GIBirdview(TEXT("Birdview")); const FName GIChargenCameras(TEXT("ChargenCameras")); const FName GIPlayerCharacter(TEXT("PlayerCharacter")); const FName GIPC(TEXT("PC")); const FName GIRoom(TEXT("Room")); const FName GISexOn(TEXT("SexOn")); // POVOn est une variable du PC (ecrite par SwitchCamera), pas de GI (CAM-COMMUN2-DIAG, KO 1). const FName PCPOVOn(TEXT("POVOn")); const FName GIBuilderOn(TEXT("BuilderOn")); // Temoin touche C (CAM-B2j) : gardes du PC (BuilderOn, SexOn, SpaceOn) et etat C1 de Room. const FName GISpaceOn(TEXT("SpaceOn")); const FName RoomC1Hidden(TEXT("C1Hidden")); const FName SwitchCameraAction(TEXT("SwitchCamera")); const FName RoomCamC1On(TEXT("CamC1On")); const FName RoomCamC3On(TEXT("CamC3On")); const FName RoomCamRoom(TEXT("CamRoom")); const FName RoomSceneCamera(TEXT("SceneCamera")); const FName RoomC2BoxMin(TEXT("C2BoxMin")); const FName RoomC2BoxMax(TEXT("C2BoxMax")); const FName RoomC2Target(TEXT("C2Target")); // Panneau de fond cache (StaticMeshActor, BlockAll) qui traverse les pieces : ignore par les sondes depuis B2g. const FName GIWall(TEXT("Wall")); // Temoin C2 (CAM-B2f) : memes depart (douille head), rayon et acteurs ignores que la sonde de Room. const TCHAR* const LabelC2 = TEXT("Camera: C2 Follow"); const FName HeadSocket(TEXT("head")); const float WitnessNearDistance = 100.f; const float WitnessProbeLength = 250.f; const float WitnessProbeRadius = 20.f; const float WitnessInterval = 1.f; // Temoin d'occlusion (CAM-Tocc) : trace toutes les 0,5 s, au plus une ligne par seconde. const float OcclusionInterval = 0.5f; const float OcclusionLogInterval = 1.f; // Sortie de CharGen (CAM-B2r) : ShowRender (GI) remet SpaceOn a faux et CloseCharGen ne le remet pas en cours // de partie (CAM-COMMUN6-DIAG § 10) ; MainMenuOn peut rester vrai si CharGen a ete ouvert du menu principal. const FName GICharGenOn(TEXT("CharGenOn")); const FName GIMainMenuOn(TEXT("MainMenuOn")); const FName GIJournalOn(TEXT("JournalOn")); const FName GIContactsOn(TEXT("ContactsOn")); const FName PCMoveCamera(TEXT("MoveCamera")); const TCHAR* const MainMenuClass = TEXT("MainMenu_C"); const float CharGenRepairDelay = 10.f; const TCHAR* const RoomCameraLabels[] = { TEXT("Camera: C1 First person"), TEXT("Camera: C2 Follow"), TEXT("Camera: C3 Ceiling"), TEXT("Camera: C3 Room (old)") }; // Temoin C1 (CAM-B2r) : ecart de FirstPersonSM aux yeux et a l'horizontale, toutes les 0,5 s, au plus une ligne par seconde. const TCHAR* const LabelC1 = TEXT("Camera: C1 First person"); const FName PlayerFirstPersonSM(TEXT("FirstPersonSM")); const FName PlayerOccupyingFnt(TEXT("OccupyingFnt")); const FName RoomCamTimer(TEXT("RoomCamTimer")); const FName LeftEyeSocket(TEXT("lEye")); const FName RightEyeSocket(TEXT("rEye")); const float C1Interval = 0.5f; const float C1LogInterval = 1.f; const float C1MaxEyeDistance = 8.f; const float C1MaxPitch = 25.f; const float C1MaxYawGap = 30.f; // Camera de scene au clavier (CAM-B2p, P3) : pendant le sexe, ZQSD pilote la camera en vue (la marche de STG // est coupee pendant le sexe). Mode Scene : orbite (Q, D) et avance ou recul (Z, S) autour du couple ; // POV : lacet (Q, D) et tangage (Z, S) de FirstPersonSM de la personne en vue. Aucun mappage n'est change. const FName AxisMoveForward(TEXT("MoveForward")); const FName AxisMoveRight(TEXT("MoveRight")); const FName AxisLeftRight(TEXT("LeftRight")); const FName AxisDownUp(TEXT("DownUp")); const FName AxisScroll(TEXT("ScrollDownUp")); const FName GISexPersons(TEXT("SexPersons")); const FName GISexCameraIndex(TEXT("SexCameraIndex")); const FName RoomMoveObject(TEXT("MoveObject")); const float SexOrbitSpeed = 90.f; // degres par seconde const float SexDollySpeed = 200.f; // cm par seconde const float SexMinDistance = 80.f; const float SexMaxDistance = 800.f; const float SexLookSpeed = 90.f; // degres par seconde const float SexMaxPitch = 80.f; // P3 bis : zoom a la molette vers le couple. Desactive : la molette garde le zoom d'origine de Room (noeud 125). const bool bSexWheelZoom = false; const float SexWheelStep = 40.f; // cm par cran const float SexKeysLogInterval = 1.f; const float SexMaxStep = 0.05f; // pas de temps maximal pris en compte (s) // Couple a la souris (CAM-B2p-D) : en mode Scene, clic gauche maintenu sur le couple = glisser-deposer sur la // surface visee (lit ou sol), clic droit maintenu = rotation ; la camera en vue est figee pendant la prise. const FName PelvisSocket(TEXT("pelvis")); const FName RoomFloor(TEXT("Box")); const float CoupleSelectRadius = 120.f; // px, autour du segment bassin-tete projete const float CoupleDragThreshold = 5.f; // px avant le premier deplacement const float CoupleTurnPerPixel = 0.3f; // degres par pixel const float CoupleFloorMargin = 30.f; // cm, marge au bord du sol de Room const float CursorTraceLength = 5000.f; const float CoupleTraceUp = 150.f; // depart du trace vertical au-dessus du pivot const float CoupleTraceDown = 400.f; // fin du trace vertical sous le pivot // G-e (CAM-B2p-H) : la racine du maillage la plus basse est posee a cette hauteur au-dessus de la surface visee. const float CoupleRootAboveSurface = 3.f; // Borne dure du couple (CAM-B2p-E, CAM-COMMUN9-DIAG) : pendant le sexe, a chaque image, quelle que soit la source // du deplacement (fleches, molette, glisser, jeu). Pivot dans le sol de Room.Box moins la marge ; racine du // maillage de chaque personne jamais sous le dessus du sol (tolerance) ni au-dessus d'un plafond estime. const float CoupleFloorTolerance = 5.f; // cm sous le dessus du sol toleres pour la racine du maillage const float CoupleCeilingHeight = 350.f; // cm au-dessus du sol (plafond estime : Room n'expose pas de plafond) const float CoupleHeadroom = 120.f; // cm entre la racine du maillage la plus haute et le plafond (corps assis ou allonge) const float CoupleMoveWitness = 5.f; // cm par image hors prise au-dela desquels le temoin ecrit const float CoupleBoundsLogInterval = 1.f; // Interieur reel de la piece (CAM-B2p-F, CAM-COMMUN9-DIAG § 6) : Room.Box deborde des murs. Murs = composants de // Room nommes Wall* (« bloque par Room_C_0/Wall2 », LPShowCouple) ; repli : structure AllComponents[0..5] (resizeRoom // ne compense l'echelle qu'a partir de l'indice 6), puis demi-cote d'echelle de la racine. const TCHAR* const WallPrefix = TEXT("Wall"); const FName RoomAllComponents(TEXT("AllComponents")); const int32 RoomStructureCount = 6; const float WallMinHeight = 100.f; // un mur fait plus de 100 cm de haut const float WallThinRatio = 0.25f; // petit cote horizontal sous le quart du grand const float CeilingMaxThickness = 50.f; const float CeilingMinHeight = 150.f; // au-dessus du sol const float RootHalfSizePerScale = 50.f; // demi-cote du maillage racine par unite d'echelle (estime : carre de 100 cm) // Marge aux murs = demi-boite du couple (Show couple : etendue X=78 Y=46) : la boite ne touche jamais un mur. const float CoupleWallMargin = 80.f; // Pose libre (G1) : boite du couple testee a la place visee, canal Pawn, composants de Room seulement, sol et lits admis. const FVector CoupleProbeExtent(78.0, 46.0, 50.0); const float CoupleProbeAboveRoot = 58.f; // centre de la boite au-dessus de la racine du maillage (bas a +8 cm) // Recadrage et suivi du couple (CAM-B2p-G) : la camera de scene vise le milieu du corps, a distance par defaut ; // elle suit les deplacements du couple hors glisser ; C3 (variable SexC3 du PC) recadre. const FName PCSexC3(TEXT("SexC3")); const float RecenterDistance = 300.f; const float RecenterHeight = 120.f; const float RecenterTargetAboveRoot = 60.f; const float RecenterWallMargin = 20.f; const float FollowMaxDistance = 900.f; // Le pivot est la place des acteurs (l'animation ne la bouge pas) : tout pas compte, meme lent (VAL B2p-G N1). const float FollowMinStep = 0.01f; const float RecenterMinDistance = 50.f; // cm : distance minimale a la cible apres un obstacle (VAL B2p-G N2) // G-g (CAM-B2p-H) : clic molette court pendant le sexe = bascule du panneau des positions, par l'evenement // LeftMouseSexOn du PC (le meme que la voie d'Espace). Une glissee a la molette reste l'orbite. const FName PCLeftMouseSexOn(TEXT("LeftMouseSexOn")); const float PanelClickMaxHold = 0.2f; // s, comme HoldTime du PC const float PanelClickMaxMove = 5.f; // px // Aide des controles du sexe (CAM-B2p-I) : icone « i » (texture de FAQ du menu principal) dans le panneau des actions // (Cursor), a cote du filtre SearchAction ; les 4 lignes d'origine (SexInterface.TextBlock_1) sont rendues invisibles. const FName GISexInterface(TEXT("SexInterface")); const FName SexHelpLinesName(TEXT("TextBlock_1")); const TCHAR* const SexHelpLinesStart = TEXT("Click any mouse button"); const FName PCCursorREF(TEXT("CursorREF")); const FName CursorSearchAction(TEXT("SearchAction")); const FName CursorBelowCursor(TEXT("BelowCursor")); const FName HelpButtonName(TEXT("LPControlsHelp")); const TCHAR* const HelpIconPath = TEXT("/Game/Heavy/New/info.info"); const float HelpIconSize = 28.f; const int32 HelpZOrder = 60; // Copie de LPDirectControlSubsystem.cpp (MenuClasses) : menus pendant lesquels le texte est masque. const TCHAR* const MenuClasses[] = { TEXT("Journal_C"), TEXT("MainMenu_C"), TEXT("CityMenu_C"), TEXT("StatMenu_C"), TEXT("ContactMenu_C"), TEXT("Inventory_C"), TEXT("CharGen_C"), TEXT("ModManager_C"), TEXT("BodyShapeMenu_C") }; UObject* ReadObject(const UObject* Container, FName Name) { const FObjectPropertyBase* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop ? Prop->GetObjectPropertyValue_InContainer(Container) : nullptr; } bool ReadBool(const UObject* Container, FName Name) { const FBoolProperty* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop && Prop->GetPropertyValue_InContainer(Container); } // Ecrit un booleen par reflexion ; rien si la propriete est absente. void WriteBool(UObject* Container, FName Name, bool bValue) { const FBoolProperty* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; if (Prop) { Prop->SetPropertyValue_InContainer(Container, bValue); } } // Poignee de minuterie (FTimerHandle) active ; faux si la propriete est absente ou d'un autre type. bool TimerActive(const UObject* Container, FName Name, const UWorld* World) { const FStructProperty* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; if (!Prop || !World || Prop->Struct != FTimerHandle::StaticStruct()) { return false; } return World->GetTimerManager().IsTimerActive(*Prop->ContainerPtrToValuePtr(Container)); } int32 ReadInt(const UObject* Container, FName Name) { const FIntProperty* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; return Prop ? Prop->GetPropertyValue_InContainer(Container) : -1; } // Acteurs valides du tableau d'objets Name de Container. TArray ReadActors(const UObject* Container, FName Name) { TArray Out; const FArrayProperty* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; const FObjectPropertyBase* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (!Inner) { return Out; } FScriptArrayHelper Array(Prop, Prop->ContainerPtrToValuePtr(Container)); for (int32 Index = 0; Index < Array.Num(); ++Index) { AActor* Actor = Cast(Inner->GetObjectPropertyValue(Array.GetRawPtr(Index))); if (IsValid(Actor)) { Out.Add(Actor); } } return Out; } // Copie de LPDirectControlSubsystem.cpp (AxisValue) : etat brut des touches de l'axe, mappages et remappages suivis. float AxisValue(const APlayerController* PC, FName Axis) { const UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!PC || !PC->PlayerInput || !Settings) { return 0.f; } float Value = 0.f; for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) { if (Map.AxisName == Axis) { const float Down = Map.Key.IsDigital() ? (PC->PlayerInput->IsPressed(Map.Key) ? 1.f : 0.f) : PC->PlayerInput->GetKeyValue(Map.Key); Value += Down * Map.Scale; } } return FMath::Clamp(Value, -1.f, 1.f); } // Noms des touches du clavier de l'action ou de l'axe Name, mappages en cours (comme AxisValue, remappages suivis) ; // Sign : 1 ou -1 = echelle de l'axe de ce signe, 0 = toutes. Souris, manette et axes analogiques exclus. FString MappedKeys(FName Name, int32 Sign) { TArray Names; if (const UInputSettings* Settings = UInputSettings::GetInputSettings()) { for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) { const bool bSign = Sign == 0 || (Sign > 0 ? Map.Scale > 0.f : Map.Scale < 0.f); if (Map.AxisName == Name && bSign && !Map.Key.IsGamepadKey() && !Map.Key.IsMouseButton() && !Map.Key.IsAxis1D()) { Names.AddUnique(Map.Key.GetDisplayName(false).ToString()); } } for (const FInputActionKeyMapping& Map : Settings->GetActionMappings()) { if (Map.ActionName == Name && !Map.Key.IsGamepadKey() && !Map.Key.IsMouseButton() && !Map.Key.IsAxis1D()) { Names.AddUnique(Map.Key.GetDisplayName(false).ToString()); } } } return Names.Num() > 0 ? FString::Join(Names, TEXT("/")) : FString(TEXT("?")); } // Empreinte des mappages en cours (actions et axes) : nombres et CRC, pour verifier qu'une scene ne les change pas. FString MappingSignature() { const UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!Settings) { return FString(TEXT("sans reglages d'entree")); } FString Joined; for (const FInputActionKeyMapping& Map : Settings->GetActionMappings()) { Joined += FString::Printf(TEXT("%s=%s,%d%d%d;"), *Map.ActionName.ToString(), *Map.Key.ToString(), Map.bShift ? 1 : 0, Map.bCtrl ? 1 : 0, Map.bAlt ? 1 : 0); } for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) { Joined += FString::Printf(TEXT("%s=%s,%.2f;"), *Map.AxisName.ToString(), *Map.Key.ToString(), Map.Scale); } return FString::Printf(TEXT("%d actions, %d axes, crc %08X"), Settings->GetActionMappings().Num(), Settings->GetAxisMappings().Num(), FCrc::StrCrc32(*Joined)); } bool ReadVector(const UObject* Container, FName Name, FVector& Out) { const FStructProperty* Prop = Container ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; if (!Prop || Prop->Struct != TBaseStructure::Get()) { return false; } Out = *Prop->ContainerPtrToValuePtr(Container); return true; } // Vrai si le tableau d'objets Name de Container contient Value. bool ArrayContains(const UObject* Container, FName Name, const UObject* Value) { const FArrayProperty* Prop = (Container && Value) ? CastField(Container->GetClass()->FindPropertyByName(Name)) : nullptr; const FObjectPropertyBase* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (!Inner) { return false; } FScriptArrayHelper Array(Prop, Prop->ContainerPtrToValuePtr(Container)); for (int32 Index = 0; Index < Array.Num(); ++Index) { if (Inner->GetObjectPropertyValue(Array.GetRawPtr(Index)) == Value) { return true; } } return false; } // Copie de LPDirectControlSubsystem.cpp (MenuOnScreen), l'original n'est pas modifie. bool MenuOnScreen(const UGameInstance* GI) { if (!GI) { return false; } if (const ULPKeyboardSubsystem* Keys = GI->GetSubsystem()) { if (Keys->IsMenuOpen()) { return true; } } for (TObjectIterator It; It; ++It) { UUserWidget* Widget = *It; if (!Widget || Widget->HasAnyFlags(RF_ClassDefaultObject) || Widget->GetGameInstance() != GI || !Widget->IsInViewport()) { continue; } const ESlateVisibility Visibility = Widget->GetVisibility(); if (Visibility == ESlateVisibility::Collapsed || Visibility == ESlateVisibility::Hidden) { continue; } const FString ClassName = Widget->GetClass()->GetName(); for (const TCHAR* Menu : MenuClasses) { if (ClassName == Menu) { return true; } } } if (!FSlateApplication::IsInitialized()) { return false; } TSharedPtr Focused = FSlateApplication::Get().GetKeyboardFocusedWidget(); return Focused.IsValid() && Focused->GetTypeAsString().Contains(TEXT("Editable")); } // Interieur de la piece : XY entre les murs (bornes interieures), Z du dessus du sol au plafond. Source ecrite dans // OutSource. Faux sans sol (Room.Box). struct FRoomInterior { FBox2D Area = FBox2D(ForceInit); double FloorTop = 0.0; double Ceiling = 0.0; int32 Walls = 0; bool bCeilingFound = false; FString Source; FBox FloorBox = FBox(ForceInit); FVector Scale = FVector::OneVector; }; bool ComputeRoomInterior(const AActor* Room, FRoomInterior& Out) { const UPrimitiveComponent* Floor = Room ? Cast(ReadObject(Room, RoomFloor)) : nullptr; if (!IsValid(Floor)) { return false; } Out.FloorBox = Floor->Bounds.GetBox(); Out.FloorTop = Out.FloorBox.Max.Z; Out.Ceiling = Out.FloorTop + CoupleCeilingHeight; Out.Area = FBox2D(FVector2D(Out.FloorBox.Min), FVector2D(Out.FloorBox.Max)); const FVector2D Center = Out.Area.GetCenter(); const USceneComponent* RootComponent = Room->GetRootComponent(); Out.Scale = RootComponent ? RootComponent->GetComponentScale() : FVector::OneVector; // Un mur resserre la borne de son cote ; un plafond donne la borne haute. FBox2D Walled = Out.Area; auto UseStructure = [&](const UPrimitiveComponent* Comp) -> bool { if (!IsValid(Comp) || Comp == Floor) { return false; } const FBox Bounds = Comp->Bounds.GetBox(); const FVector Size = Bounds.GetSize(); if (Size.Z < CeilingMaxThickness && Bounds.GetCenter().Z > Out.FloorTop + CeilingMinHeight) { Out.Ceiling = Out.bCeilingFound ? FMath::Min(Out.Ceiling, Bounds.Min.Z) : Bounds.Min.Z; Out.bCeilingFound = true; return false; } if (Size.Z < WallMinHeight || FMath::Min(Size.X, Size.Y) > WallThinRatio * FMath::Max(Size.X, Size.Y)) { return false; } if (Size.X < Size.Y) { if (Bounds.GetCenter().X < Center.X) { Walled.Min.X = FMath::Max(Walled.Min.X, Bounds.Max.X); } else { Walled.Max.X = FMath::Min(Walled.Max.X, Bounds.Min.X); } } else if (Bounds.GetCenter().Y < Center.Y) { Walled.Min.Y = FMath::Max(Walled.Min.Y, Bounds.Max.Y); } else { Walled.Max.Y = FMath::Min(Walled.Max.Y, Bounds.Min.Y); } return true; }; TInlineComponentArray Components; Room->GetComponents(Components); for (const UPrimitiveComponent* Comp : Components) { if (Comp && Comp->GetName().StartsWith(WallPrefix) && UseStructure(Comp)) { ++Out.Walls; } } Out.Source = TEXT("murs Wall*"); if (Out.Walls == 0) { // Repli 1 : structure de la piece (6 premiers composants de AllComponents). const FArrayProperty* Prop = CastField(Room->GetClass()->FindPropertyByName(RoomAllComponents)); const FObjectPropertyBase* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (Inner) { FScriptArrayHelper Array(Prop, Prop->ContainerPtrToValuePtr(Room)); for (int32 Index = 0; Index < FMath::Min(Array.Num(), RoomStructureCount); ++Index) { if (UseStructure(Cast(Inner->GetObjectPropertyValue(Array.GetRawPtr(Index))))) { ++Out.Walls; } } } Out.Source = TEXT("structure AllComponents"); } if (Out.Walls == 0) { // Repli 2 : demi-cote d'echelle de la racine, centre sur Room, dans la boite du sol. const FVector Location = Room->GetActorLocation(); const double HalfX = FMath::Abs(Out.Scale.X) * RootHalfSizePerScale; const double HalfY = FMath::Abs(Out.Scale.Y) * RootHalfSizePerScale; Walled.Min.X = FMath::Max(Walled.Min.X, Location.X - HalfX); Walled.Max.X = FMath::Min(Walled.Max.X, Location.X + HalfX); Walled.Min.Y = FMath::Max(Walled.Min.Y, Location.Y - HalfY); Walled.Max.Y = FMath::Min(Walled.Max.Y, Location.Y + HalfY); Out.Source = TEXT("echelle de la racine"); } if (Walled.Max.X - Walled.Min.X > 1.0 && Walled.Max.Y - Walled.Min.Y > 1.0) { Out.Area = Walled; } else { Out.Source = TEXT("sol (murs incoherents)"); } return true; } // Z de la racine du maillage la plus basse du couple (acteur a defaut de maillage). double CoupleLowestRoot(const TArray& Couple) { double Lowest = TNumericLimits::Max(); for (const AActor* Person : Couple) { const ACharacter* Character = Cast(Person); const USkeletalMeshComponent* Mesh = Character ? Character->GetMesh() : nullptr; Lowest = FMath::Min(Lowest, Mesh ? Mesh->GetComponentLocation().Z : Person->GetActorLocation().Z); } return Lowest; } // Maillage statique du composant, ou nul. const UStaticMesh* ComponentMesh(const UPrimitiveComponent* Comp) { const UStaticMeshComponent* MeshComp = Cast(Comp); return MeshComp ? MeshComp->GetStaticMesh() : nullptr; } // Lit : meme regle que LPShowCoupleSubsystem (IsBedMesh), recopiee ici (l'original est dans un espace anonyme). bool IsAdmittedBed(const UPrimitiveComponent* Comp) { const UStaticMesh* Mesh = ComponentMesh(Comp); const FString MeshName = Mesh ? Mesh->GetName() : FString(); return Mesh && (MeshName.Contains(TEXT("bed"), ESearchCase::IgnoreCase) || MeshName.Contains(TEXT("fnt_ext011")) || MeshName.Equals(TEXT("fnt_new045")) || MeshName.Equals(TEXT("fnt_new235"))); } // « Proprietaire/composant (maillage) » pour les lignes de refus (CAM-B2p-H, SPEC G § 5.3). FString DescribeComponent(const UPrimitiveComponent* Comp) { const UStaticMesh* Mesh = ComponentMesh(Comp); return FString::Printf(TEXT("%s/%s (%s)"), *GetNameSafe(Comp ? Comp->GetOwner() : nullptr), *GetNameSafe(Comp), Mesh ? *Mesh->GetName() : TEXT("sans maillage statique")); } // Pose libre (G1) : la boite du couple a cette place ne recouvre aucun composant bloquant de Room, hors sol et lits. bool CoupleSpotFree(const UWorld* World, const AActor* Room, const UPrimitiveComponent* Floor, const TArray& Couple, const FVector& RootSpot, double Yaw, FString& OutBlocker) { if (!World || !Room) { return true; } FCollisionQueryParams Query(SCENE_QUERY_STAT(LPCoupleSpot), false); Query.AddIgnoredActors(Couple); if (Floor) { Query.AddIgnoredComponent(Floor); } TArray Overlaps; const FVector Center(RootSpot.X, RootSpot.Y, RootSpot.Z + CoupleProbeAboveRoot); World->OverlapMultiByChannel(Overlaps, Center, FRotator(0.0, Yaw, 0.0).Quaternion(), ECC_Pawn, FCollisionShape::MakeBox(CoupleProbeExtent), Query); for (const FOverlapResult& Overlap : Overlaps) { const UPrimitiveComponent* Comp = Overlap.GetComponent(); if (!Overlap.bBlockingHit || !Comp || Comp->GetOwner() != Room) { continue; } // Lits franchissables : meme regle que LPShowCoupleSubsystem (IsBedMesh). if (IsAdmittedBed(Comp)) { continue; } OutBlocker = DescribeComponent(Comp); return false; } return true; } // Champ de texte avec le focus clavier (ex. « Type keyword to filter » du menu des positions). TSharedPtr FocusedEditable() { if (!FSlateApplication::IsInitialized()) { return nullptr; } TSharedPtr Focused = FSlateApplication::Get().GetKeyboardFocusedWidget(); return Focused.IsValid() && Focused->GetTypeAsString().Contains(TEXT("Editable")) ? Focused : nullptr; } // Un widget de la classe ClassName est dans le viewport de GI et visible. bool WidgetOnScreen(const UGameInstance* GI, const TCHAR* ClassName) { for (TObjectIterator It; It && GI; ++It) { const UUserWidget* Widget = *It; if (!Widget || Widget->HasAnyFlags(RF_ClassDefaultObject) || Widget->GetGameInstance() != GI || !Widget->IsInViewport()) { continue; } const ESlateVisibility Visibility = Widget->GetVisibility(); if (Visibility != ESlateVisibility::Collapsed && Visibility != ESlateVisibility::Hidden && Widget->GetClass()->GetName() == ClassName) { return true; } } return false; } } void ULPCameraModeSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPCameraModeSubsystem::Tick), LPCameraModeLocal::TickInterval); // A chaque image : un appui bref sur C tient moins de 0,1 s. SwitchKeyHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPCameraModeSubsystem::TickSwitchKey), 0.f); } void ULPCameraModeSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(SwitchKeyHandle); FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); RemoveWidget(); RemoveHelpBox(); Super::Deinitialize(); } bool ULPCameraModeSubsystem::Tick(float DeltaTime) { // Avant les retours de l'indicateur : le filtre du panneau qui a le focus compte comme un menu pour MenuOnScreen. TickControlsHelp(); const FString Label = ComputeLabel(); const bool bMenu = LPCameraModeLocal::MenuOnScreen(GetGameInstance()); bMenuOnScreen = bMenu; RepairAfterCharGen(Label, bMenu, DeltaTime); if (!Label.IsEmpty()) { LastEmptyView.Reset(); } else if (!bMenu) { // Temoin touche C (CAM-B2j) : vue non reconnue hors menu, une ligne par changement de vue. const UWorld* World = GetGameInstance() ? GetGameInstance()->GetWorld() : nullptr; const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const AActor* View = PC ? PC->GetViewTarget() : nullptr; const FString ViewText = FString::Printf(TEXT("%s, classe %s"), *GetNameSafe(View), View ? *View->GetClass()->GetName() : TEXT("-")); if (ViewText != LastEmptyView) { LastEmptyView = ViewText; ShownLabel.Reset(); UE_LOG(LogLifePlay, Log, TEXT("Indicateur de camera : aucun (vue %s)"), *ViewText); } } if (Label.IsEmpty() || bMenu) { SetShown(false); return true; } if (!EnsureWidget()) { return true; } if (Label != ShownLabel) { Text->SetText(FText::FromString(Label)); ShownLabel = Label; UE_LOG(LogLifePlay, Log, TEXT("Indicateur de camera : %s"), *Label); } SetShown(true); if (Label == LPCameraModeLocal::LabelC2) { WitnessC2(DeltaTime); WitnessOcclusion(DeltaTime); } else if (Label == LPCameraModeLocal::LabelC1) { WitnessC1(DeltaTime); } return true; } void ULPCameraModeSubsystem::RepairAfterCharGen(const FString& Label, bool bMenu, float DeltaTime) { UGameInstance* GI = GetGameInstance(); if (!GI) { return; } if (LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GICharGenOn)) { bCharGenSeen = true; CharGenRepairTimeout = LPCameraModeLocal::CharGenRepairDelay; return; } if (!bCharGenSeen) { return; } const UWorld* World = GI->GetWorld(); const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const AActor* View = PC ? PC->GetViewTarget() : nullptr; bool bRoomCamera = false; for (const TCHAR* RoomLabel : LPCameraModeLocal::RoomCameraLabels) { bRoomCamera |= Label == RoomLabel; } if (!bRoomCamera || bMenu) { CharGenRepairTimeout -= DeltaTime; if (CharGenRepairTimeout <= 0.f) { bCharGenSeen = false; UE_LOG(LogLifePlay, Log, TEXT("CharGen : sortie sans camera de piece (vue %s, indicateur '%s'), rien remis"), *GetNameSafe(View), *Label); } return; } // Vue revenue sur une camera de piece (jamais Birdview, la carte de la ville) : une seule reparation. bCharGenSeen = false; if (!LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISpaceOn)) { LPCameraModeLocal::WriteBool(GI, LPCameraModeLocal::GISpaceOn, true); UE_LOG(LogLifePlay, Log, TEXT("CharGen : SpaceOn remis a vrai (vue %s, indicateur '%s')"), *GetNameSafe(View), *Label); } if (LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GIMainMenuOn) && !LPCameraModeLocal::WidgetOnScreen(GI, LPCameraModeLocal::MainMenuClass)) { LPCameraModeLocal::WriteBool(GI, LPCameraModeLocal::GIMainMenuOn, false); UE_LOG(LogLifePlay, Log, TEXT("CharGen : MainMenuOn remis a faux (menu principal ferme)")); } } void ULPCameraModeSubsystem::WitnessC1(float DeltaTime) { C1Cooldown = FMath::Max(0.f, C1Cooldown - DeltaTime); if (C1Cooldown > 0.f) { return; } C1Cooldown = LPCameraModeLocal::C1Interval; const UGameInstance* GI = GetGameInstance(); const UWorld* World = GI ? GI->GetWorld() : nullptr; const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const ACharacter* Player = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter)); const USkeletalMeshComponent* Mesh = IsValid(Player) ? Player->GetMesh() : nullptr; const USceneComponent* Camera = IsValid(Player) ? Cast(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerFirstPersonSM)) : nullptr; if (!PC || !Mesh || !IsValid(Camera)) { return; } const FVector Eyes = (Mesh->GetSocketLocation(LPCameraModeLocal::LeftEyeSocket) + Mesh->GetSocketLocation(LPCameraModeLocal::RightEyeSocket)) * 0.5f; const double Distance = FVector::Dist(Camera->GetComponentLocation(), Eyes); const FRotator CameraRotation = Camera->GetComponentRotation(); const double YawGap = FRotator::NormalizeAxis(CameraRotation.Yaw - PC->GetControlRotation().Yaw); if (Distance <= LPCameraModeLocal::C1MaxEyeDistance && FMath::Abs(CameraRotation.Pitch) <= LPCameraModeLocal::C1MaxPitch && FMath::Abs(YawGap) <= LPCameraModeLocal::C1MaxYawGap) { return; } const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom); C1Cooldown = LPCameraModeLocal::C1LogInterval; UE_LOG(LogLifePlay, Log, TEXT("Temoin C1 : camera a %.0f cm des yeux, tangage %+.0f, ecart de lacet %+.0f, assis=%d, minuterie=%d"), Distance, CameraRotation.Pitch, YawGap, IsValid(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerOccupyingFnt)) ? 1 : 0, LPCameraModeLocal::TimerActive(IsValid(RoomObject) ? RoomObject : nullptr, LPCameraModeLocal::RoomCamTimer, World) ? 1 : 0); } bool ULPCameraModeSubsystem::TickSwitchKey(float DeltaTime) { const UGameInstance* GI = GetGameInstance(); const UWorld* World = GI ? GI->GetWorld() : nullptr; const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const UInputSettings* Settings = UInputSettings::GetInputSettings(); bool bDown = false; if (PC && PC->PlayerInput && Settings) { for (const FInputActionKeyMapping& Map : Settings->GetActionMappings()) { if (Map.ActionName == LPCameraModeLocal::SwitchCameraAction && PC->PlayerInput->IsPressed(Map.Key)) { bDown = true; break; } } } if (bDown && !bSwitchKeyDown) { LogSwitchKey(PC); } bSwitchKeyDown = bDown; TickSexCamera(DeltaTime); TickCoupleBounds(DeltaTime); return true; } void ULPCameraModeSubsystem::TickCoupleBounds(float DeltaTime) { CoupleBoundsCooldown = FMath::Max(0.f, CoupleBoundsCooldown - DeltaTime); CoupleMoveCooldown = FMath::Max(0.f, CoupleMoveCooldown - DeltaTime); UGameInstance* GI = GetGameInstance(); const bool bGrabEnded = bGrabJustEnded; bGrabJustEnded = false; if (!GI || !LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn)) { bHasLastPivot = false; bHiddenRoomWarned = false; bBoundsLogged = false; // B2p-G : hors sexe, C3 n'a plus de sens (variable du PC remise a faux si la scene a fini en C3). LastSexView.Reset(); bSexC3Was = false; const UWorld* EndWorld = GI ? GI->GetWorld() : nullptr; if (APlayerController* EndPC = EndWorld ? EndWorld->GetFirstPlayerController() : nullptr) { if (LPCameraModeLocal::ReadBool(EndPC, LPCameraModeLocal::PCSexC3)) { LPCameraModeLocal::WriteBool(EndPC, LPCameraModeLocal::PCSexC3, false); } } return; } AActor* Room = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom)); const TArray Couple = LPCameraModeLocal::ReadActors(GI, LPCameraModeLocal::GISexPersons); if (!IsValid(Room) || Couple.Num() == 0) { bHasLastPivot = false; return; } FVector Pivot = FVector::ZeroVector; for (const AActor* Person : Couple) { Pivot += Person->GetActorLocation(); } Pivot /= Couple.Num(); // E4 : temoin des deplacements hors prise (source des descentes de COMMUN-9 a designer). // F4 : l'image ou une prise vient de finir porte encore le dernier pas du glisser, elle n'est pas comptee. if (bHasLastPivot && CoupleGrabMode == 0 && !bGrabEnded && FVector::Dist(Pivot, LastCouplePivot) > LPCameraModeLocal::CoupleMoveWitness && CoupleMoveCooldown <= 0.f) { CoupleMoveCooldown = LPCameraModeLocal::CoupleBoundsLogInterval; const UWorld* World = GI->GetWorld(); const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const bool bShift = PC && PC->PlayerInput && (PC->PlayerInput->IsPressed(EKeys::LeftShift) || PC->PlayerInput->IsPressed(EKeys::RightShift)); const FVector Moved = Pivot - LastCouplePivot; UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple deplace hors prise de (%+.0f, %+.0f, %+.0f) ; fleches=%+.0f/%+.0f Maj=%d molette=%+.1f ; Room cachee=%d"), Moved.X, Moved.Y, Moved.Z, LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisLeftRight), LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisDownUp), bShift ? 1 : 0, LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisScroll), Room->IsHidden() ? 1 : 0); } // Piece cachee (scene d'exterieur) : pas de sol de reference, pas de borne. const UPrimitiveComponent* Floor = Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomFloor)); if (Room->IsHidden() || !IsValid(Floor)) { if (!bHiddenRoomWarned) { bHiddenRoomWarned = true; UE_LOG(LogLifePlay, Log, TEXT("Sexe : pas de borne du couple (Room cachee=%d, sol %s)"), Room->IsHidden() ? 1 : 0, *GetNameSafe(Floor)); } TickSceneCamera(GI, Room, Couple, Pivot, bGrabEnded); LastCouplePivot = Pivot; bHasLastPivot = true; return; } // E1 + F1 : borne XY du pivot entre les murs reels (marge = demi-boite du couple), puis Z de la racine du maillage // de chaque personne entre le dessus du sol et le plafond (trouve ou estime). LPCameraModeLocal::FRoomInterior Interior; if (!LPCameraModeLocal::ComputeRoomInterior(Room, Interior)) { LastCouplePivot = Pivot; bHasLastPivot = true; return; } const double FloorTop = Interior.FloorTop; const double Margin = LPCameraModeLocal::CoupleWallMargin; if (!bBoundsLogged) { // F2 : bornes retenues, une fois par scene. bBoundsLogged = true; UE_LOG(LogLifePlay, Log, TEXT("Sexe : bornes du couple X [%.0f, %.0f] Y [%.0f, %.0f] sol %.0f plafond %.0f (marge %.0f) ; source %s, murs %d, plafond trouve %d ; sol Box X [%.0f, %.0f] Y [%.0f, %.0f] ; echelle racine (%.2f, %.2f)"), Interior.Area.Min.X, Interior.Area.Max.X, Interior.Area.Min.Y, Interior.Area.Max.Y, FloorTop, Interior.Ceiling, Margin, *Interior.Source, Interior.Walls, Interior.bCeilingFound ? 1 : 0, Interior.FloorBox.Min.X, Interior.FloorBox.Max.X, Interior.FloorBox.Min.Y, Interior.FloorBox.Max.Y, Interior.Scale.X, Interior.Scale.Y); } FVector Correction = FVector::ZeroVector; const FBox2D& Area = Interior.Area; Correction.X = Area.Max.X - Area.Min.X > 2.0 * Margin ? FMath::Clamp(Pivot.X, Area.Min.X + Margin, Area.Max.X - Margin) - Pivot.X : Area.GetCenter().X - Pivot.X; Correction.Y = Area.Max.Y - Area.Min.Y > 2.0 * Margin ? FMath::Clamp(Pivot.Y, Area.Min.Y + Margin, Area.Max.Y - Margin) - Pivot.Y : Area.GetCenter().Y - Pivot.Y; double LowestRoot = TNumericLimits::Max(); double HighestRoot = TNumericLimits::Lowest(); for (const AActor* Person : Couple) { const ACharacter* Character = Cast(Person); const USkeletalMeshComponent* Mesh = Character ? Character->GetMesh() : nullptr; const double RootZ = Mesh ? Mesh->GetComponentLocation().Z : Person->GetActorLocation().Z; LowestRoot = FMath::Min(LowestRoot, RootZ); HighestRoot = FMath::Max(HighestRoot, RootZ); } if (LowestRoot < FloorTop - LPCameraModeLocal::CoupleFloorTolerance) { Correction.Z = FloorTop - LowestRoot; } else if (HighestRoot > Interior.Ceiling - LPCameraModeLocal::CoupleHeadroom) { Correction.Z = Interior.Ceiling - LPCameraModeLocal::CoupleHeadroom - HighestRoot; } if (Correction.SizeSquared() > 0.25) { for (AActor* Person : Couple) { Person->SetActorLocation(Person->GetActorLocation() + Correction); } Pivot += Correction; if (CoupleBoundsCooldown <= 0.f) { CoupleBoundsCooldown = LPCameraModeLocal::CoupleBoundsLogInterval; UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple ramene dans la piece de (%+.0f, %+.0f, %+.0f) ; sol %.0f, plafond %.0f (racine au plus %.0f) ; prise=%d"), Correction.X, Correction.Y, Correction.Z, FloorTop, Interior.Ceiling, Interior.Ceiling - LPCameraModeLocal::CoupleHeadroom, CoupleGrabMode); } } TickSceneCamera(GI, Room, Couple, Pivot, bGrabEnded); LastCouplePivot = Pivot; bHasLastPivot = true; } void ULPCameraModeSubsystem::TickSceneCamera(UGameInstance* GI, AActor* Room, const TArray& Couple, const FVector& Pivot, bool bGrabEnded) { const UWorld* World = GI->GetWorld(); const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; AActor* View = PC ? PC->GetViewTarget() : nullptr; AActor* SceneCamera = Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomSceneCamera)); const bool bC3 = LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCSexC3); const bool bOnScene = IsValid(SceneCamera) && View == SceneCamera; if (bOnScene && LastSexView.Get() != SceneCamera) { // G-b : entree en mode Scene (debut de scene, retour depuis le dernier POV). RecenterSceneCamera(GI, Room, SceneCamera, Couple, Pivot, TEXT("entree en Scene")); } else if (bOnScene && bC3 && !bSexC3Was) { // G-d : passage en C3 (variable SexC3 du PC) : recadrage. RecenterSceneCamera(GI, Room, SceneCamera, Couple, Pivot, TEXT("C3")); } else if (bOnScene && bHasLastPivot && CoupleGrabMode == 0 && !bGrabEnded) { // G-c : le couple a bouge hors glisser (fleches, molette, borne, jeu) : la camera suit du meme vecteur. const FVector Moved = Pivot - LastCouplePivot; if (Moved.Size() > LPCameraModeLocal::FollowMinStep) { SceneCamera->AddActorWorldOffset(Moved); } // Distance douce : jamais plus de FollowMaxDistance du couple. const FVector Away = SceneCamera->GetActorLocation() - Pivot; if (Away.Size() > LPCameraModeLocal::FollowMaxDistance) { SceneCamera->SetActorLocation(Pivot + Away.GetSafeNormal() * LPCameraModeLocal::FollowMaxDistance); } } bSexC3Was = bC3; LastSexView = View; } void ULPCameraModeSubsystem::RecenterSceneCamera(UGameInstance* GI, AActor* Room, AActor* SceneCamera, const TArray& Couple, const FVector& Pivot, const TCHAR* Reason) { const UWorld* World = GI->GetWorld(); if (!World || Couple.Num() == 0) { return; } // Cible : milieu du corps, au-dessus de la racine du maillage la plus basse du couple. double LowestRoot = TNumericLimits::Max(); for (const AActor* Person : Couple) { const ACharacter* Character = Cast(Person); const USkeletalMeshComponent* Mesh = Character ? Character->GetMesh() : nullptr; LowestRoot = FMath::Min(LowestRoot, Mesh ? Mesh->GetComponentLocation().Z : Person->GetActorLocation().Z); } const FVector Target(Pivot.X, Pivot.Y, LowestRoot + LPCameraModeLocal::RecenterTargetAboveRoot); // Azimut actuel de la camera autour du couple (saut limite), sinon derriere le meneur. FVector Flat = SceneCamera->GetActorLocation() - Target; Flat.Z = 0.0; if (!Flat.Normalize()) { Flat = -Couple[0]->GetActorForwardVector(); Flat.Z = 0.0; if (!Flat.Normalize()) { Flat = FVector::ForwardVector; } } FVector Desired = Target + Flat * LPCameraModeLocal::RecenterDistance + FVector(0.0, 0.0, LPCameraModeLocal::RecenterHeight); LPCameraModeLocal::FRoomInterior Interior; if (!Room->IsHidden() && LPCameraModeLocal::ComputeRoomInterior(Room, Interior)) { const FBox2D& Area = Interior.Area; const double Margin = LPCameraModeLocal::RecenterWallMargin; if (Area.Max.X - Area.Min.X > 2.0 * Margin && Area.Max.Y - Area.Min.Y > 2.0 * Margin) { Desired.X = FMath::Clamp(Desired.X, Area.Min.X + Margin, Area.Max.X - Margin); Desired.Y = FMath::Clamp(Desired.Y, Area.Min.Y + Margin, Area.Max.Y - Margin); } Desired.Z = FMath::Clamp(Desired.Z, Interior.FloorTop + 30.0, FMath::Max(Interior.FloorTop + 30.0, Interior.Ceiling - Margin)); } // Comme un bras de camera : devant le premier obstacle entre la cible et la camera (couple et cameras ignores). FCollisionQueryParams Query(SCENE_QUERY_STAT(LPRecenter), true); Query.AddIgnoredActors(Couple); Query.AddIgnoredActor(SceneCamera); if (AActor* Wall = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIWall))) { Query.AddIgnoredActor(Wall); } if (AActor* CamRoom = Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomCamRoom))) { Query.AddIgnoredActor(CamRoom); } FHitResult Hit; if (World->LineTraceSingleByChannel(Hit, Target, Desired, ECC_Visibility, Query)) { // Devant l'obstacle, mais jamais a moins de RecenterMinDistance de la cible (sinon la camera passerait de l'autre cote). // ObstacleReach, pas Reach : LPDirectControlSubsystem.cpp a un Reach dans son espace anonyme (C4459 en unite unifiee). const FVector Axis = (Desired - Target).GetSafeNormal(); const double ObstacleReach = FMath::Max(FVector::Dist(Target, Hit.Location) - LPCameraModeLocal::RecenterWallMargin, static_cast(LPCameraModeLocal::RecenterMinDistance)); Desired = Target + Axis * ObstacleReach; } SceneCamera->SetActorLocationAndRotation(Desired, (Target - Desired).Rotation()); UE_LOG(LogLifePlay, Log, TEXT("Sexe : vue recadree sur le couple (%s) a %.0f cm, cible (%.0f, %.0f, %.0f)%s"), Reason, FVector::Dist(Desired, Target), Target.X, Target.Y, Target.Z, Hit.bBlockingHit ? TEXT(", rapprochee devant un obstacle") : TEXT("")); } void ULPCameraModeSubsystem::TickSexCamera(float DeltaTime) { SexKeysCooldown = FMath::Max(0.f, SexKeysCooldown - DeltaTime); // Pas de temps borne : un a-coup d'image (chargement) ne fait pas tourner ou avancer la camera d'un coup. const float Step = FMath::Min(DeltaTime, LPCameraModeLocal::SexMaxStep); UGameInstance* GI = GetGameInstance(); const bool bSexOn = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn); // CAM-B2p-I (COMMUN-12, point 3) : debut et fin de scene ; a la fin, chaque etat touche par CAM est rendu, une ligne. if (bSexOn != bSexStateWas) { bSexStateWas = bSexOn; if (bSexOn) { BeginSexScene(); } else { EndSexScene(); } } if (!bSexOn) { bSceneFocusChecked = false; bFocusClickWasDown = false; bMiddleWasDown = false; } else { // H1 / H2 : un champ de texte (filtre du menu des positions) garde le focus clavier ; ZQSD partirait dedans. // H1 : un clic dans la vue 3D (vu par le PlayerInput, donc hors interface) rend le focus au viewport. // H2 : a la premiere image du sexe, un champ vide qui a le focus le rend une fois. // CAM-B2p-H : le clic molette aussi, sinon le filtre du panneau (menu pour MenuOnScreen) le bloquerait. const UWorld* FocusWorld = GI->GetWorld(); const APlayerController* FocusPC = FocusWorld ? FocusWorld->GetFirstPlayerController() : nullptr; const bool bClick = FocusPC && FocusPC->PlayerInput && (FocusPC->PlayerInput->IsPressed(EKeys::LeftMouseButton) || FocusPC->PlayerInput->IsPressed(EKeys::RightMouseButton) || FocusPC->PlayerInput->IsPressed(EKeys::MiddleMouseButton)); const TSharedPtr Editable = LPCameraModeLocal::FocusedEditable(); bool bGiveBack = false; const TCHAR* Why = TEXT(""); if (Editable.IsValid() && bClick && !bFocusClickWasDown) { bGiveBack = true; Why = TEXT("clic dans la vue"); } else if (Editable.IsValid() && !bSceneFocusChecked && Editable->GetTypeAsString() == TEXT("SEditableText") && StaticCastSharedPtr(Editable)->GetText().IsEmpty()) { bGiveBack = true; Why = TEXT("champ vide a l'entree"); } bSceneFocusChecked = true; bFocusClickWasDown = bClick; if (bGiveBack) { UE_LOG(LogLifePlay, Log, TEXT("Sexe : focus clavier rendu a la vue (%s, champ %s)"), Why, *Editable->GetTypeAsString()); FSlateApplication::Get().SetAllUserFocusToGameViewport(); bMenuOnScreen = LPCameraModeLocal::MenuOnScreen(GI); } } if (!bSexOn || bMenuOnScreen) { // Hors sexe ou menu : aucune prise en cours, les clics reviennent au jeu tels quels (clic molette compris). ReleaseCoupleGrab(); bMiddleWasDown = false; if (!GI || !LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn)) { bCursorWarned = false; } return; } const UWorld* World = GI->GetWorld(); APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; AActor* View = PC ? PC->GetViewTarget() : nullptr; if (PC) { TickPanelClick(PC, DeltaTime); KeepMouseFree(PC, DeltaTime); } if (!PC || !IsValid(View)) { ReleaseCoupleGrab(); return; } const float Forward = LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisMoveForward); const float Right = LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisMoveRight); const float Wheel = LPCameraModeLocal::bSexWheelZoom ? LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisScroll) : 0.f; const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom); const UObject* Room = IsValid(RoomObject) ? RoomObject : nullptr; const TArray Couple = LPCameraModeLocal::ReadActors(GI, LPCameraModeLocal::GISexPersons); FVector Pivot = FVector::ZeroVector; for (const AActor* Person : Couple) { Pivot += Person->GetActorLocation(); } if (Couple.Num() > 0) { Pivot /= Couple.Num(); } const bool bKeys = !FMath::IsNearlyZero(Forward) || !FMath::IsNearlyZero(Right) || !FMath::IsNearlyZero(Wheel); // Prise du couple a la souris (B2p-D) ; pendant la prise, la camera est figee et ZQSD ne la deplace pas. const bool bGrabbing = TickCoupleMouse(GI, PC, View, Room, Couple, Pivot); const AActor* SceneCamera = Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomSceneCamera)); if (!bGrabbing && bKeys && Couple.Num() > 0 && View == SceneCamera) { // Mode Scene : la camera tourne autour du couple du meme angle que son lacet (cadrage garde, comme B2n), // puis avance ou recule vers le couple, bornee. const float Angle = -Right * LPCameraModeLocal::SexOrbitSpeed * Step; FVector Offset = View->GetActorLocation() - Pivot; Offset = Offset.RotateAngleAxis(Angle, FVector::UpVector); // Bornes seulement quand on avance ou recule : l'orbite seule ne deplace pas une camera posee plus loin par le jeu. double Distance = Offset.Size(); if (!FMath::IsNearlyZero(Forward) || !FMath::IsNearlyZero(Wheel)) { Distance = FMath::Clamp(Distance - Forward * LPCameraModeLocal::SexDollySpeed * Step - Wheel * LPCameraModeLocal::SexWheelStep, static_cast(LPCameraModeLocal::SexMinDistance), static_cast(LPCameraModeLocal::SexMaxDistance)); } const FVector Direction = Offset.GetSafeNormal(); if (!Direction.IsZero()) { FRotator Rotation = View->GetActorRotation(); Rotation.Yaw += Angle; View->SetActorLocationAndRotation(Pivot + Direction * Distance, Rotation); } } else if (!bGrabbing && bKeys && Couple.Contains(View)) { // POV : meme composant que la souris en POV (PC 204 et 316 : FirstPersonSM de SexPersons[i]). USceneComponent* Look = Cast(LPCameraModeLocal::ReadObject(View, LPCameraModeLocal::PlayerFirstPersonSM)); if (IsValid(Look)) { FRotator Rotation = Look->GetComponentRotation(); Rotation.Yaw += Right * LPCameraModeLocal::SexLookSpeed * Step; Rotation.Pitch = FMath::Clamp(Rotation.Pitch + Forward * LPCameraModeLocal::SexLookSpeed * Step, -static_cast(LPCameraModeLocal::SexMaxPitch), static_cast(LPCameraModeLocal::SexMaxPitch)); Look->SetWorldRotation(Rotation); } } // Temoin (KO Maj + souris, CAM-COMMUN6-DIAG § 6) : au plus une ligne par seconde tant qu'une touche est tenue. const float Arrows = FMath::Abs(LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisLeftRight)) + FMath::Abs(LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisDownUp)); const bool bShift = PC->PlayerInput && (PC->PlayerInput->IsPressed(EKeys::LeftShift) || PC->PlayerInput->IsPressed(EKeys::RightShift)); const bool bButtons = bLeftWasDown || bRightWasDown; if (SexKeysCooldown > 0.f || (!bKeys && FMath::IsNearlyZero(Arrows) && !bShift && !bButtons)) { return; } SexKeysCooldown = LPCameraModeLocal::SexKeysLogInterval; UE_LOG(LogLifePlay, Log, TEXT("Sexe : touches avant=%+.0f cote=%+.0f fleches=%+.0f/%+.0f Maj=%d MoveObject=%d MoveCamera=%d ; boutons G=%d D=%d prise=%d ; vue %s ; index POV %d ; couple %d a (%.0f, %.0f, %.0f), lacet %.0f"), Forward, Right, LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisLeftRight), LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisDownUp), bShift ? 1 : 0, LPCameraModeLocal::ReadBool(Room, LPCameraModeLocal::RoomMoveObject) ? 1 : 0, LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCMoveCamera) ? 1 : 0, bLeftWasDown ? 1 : 0, bRightWasDown ? 1 : 0, CoupleGrabMode, *GetNameSafe(View), LPCameraModeLocal::ReadInt(GI, LPCameraModeLocal::GISexCameraIndex), Couple.Num(), Pivot.X, Pivot.Y, Pivot.Z, Couple.Num() > 0 ? Couple[0]->GetActorRotation().Yaw : 0.0); } void ULPCameraModeSubsystem::TickPanelClick(APlayerController* PC, float DeltaTime) { // CAM-B2p-I (COMMUN-12) : Alt aussi (Espace rend sa fonction au jeu). Alt et le clic molette sont deja l'action // RotateCamera : un appui bref sans mouvement bascule le panneau, un appui tenu ou une glissee reste l'orbite. const bool bMiddleButton = PC->PlayerInput && PC->PlayerInput->IsPressed(EKeys::MiddleMouseButton); const bool bAltKey = PC->PlayerInput && (PC->PlayerInput->IsPressed(EKeys::LeftAlt) || PC->PlayerInput->IsPressed(EKeys::RightAlt)); const bool bMiddle = bMiddleButton || bAltKey; float PanelMouseX = 0.f; float PanelMouseY = 0.f; const bool bPanelMouse = PC->GetMousePosition(PanelMouseX, PanelMouseY); if (bMiddle && !bMiddleWasDown) { bPanelByAlt = bAltKey && !bMiddleButton; MiddleHeld = 0.f; MiddleStart = FVector2D(PanelMouseX, PanelMouseY); bMiddleHadMouse = bPanelMouse; bMiddleMoved = false; } else if (bMiddleWasDown) { // Duree reelle (DeltaTime non borne) ; mouvement mesure seulement si le curseur a une position au depart et ici. MiddleHeld += DeltaTime; bMiddleMoved |= bMiddleHadMouse && bPanelMouse && FVector2D::Distance(MiddleStart, FVector2D(PanelMouseX, PanelMouseY)) > LPCameraModeLocal::PanelClickMaxMove; if (!bMiddle && MiddleHeld <= LPCameraModeLocal::PanelClickMaxHold && !bMiddleMoved) { UFunction* Toggle = PC->FindFunction(LPCameraModeLocal::PCLeftMouseSexOn); if (Toggle && Toggle->NumParms == 0) { UE_LOG(LogLifePlay, Log, TEXT("Sexe : panneau bascule (%s, %.2f s)"), bPanelByAlt ? TEXT("Alt") : TEXT("clic molette"), MiddleHeld); PC->ProcessEvent(Toggle, nullptr); } else { UE_LOG(LogLifePlay, Log, TEXT("Sexe : panneau non bascule, evenement LeftMouseSexOn absent du PC")); } } } bMiddleWasDown = bMiddle; } void ULPCameraModeSubsystem::KeepMouseFree(APlayerController* PC, float DeltaTime) { // COMMUN-12 : a la fermeture du panneau, le PC repasse en GameOnly (CapturePermanently, LockOnCapture, mouvement // haute precision) : le curseur est cache et sa position figee pendant l'appui, glisser et rotation valent 0. Avant // B2p-G, un clic court ouvrait le panneau (GameAndUI, DoNotLock) et la souris restait libre. Pendant le sexe, hors // menu et sans bouton tenu, CAM remet la souris dans cet etat ; le clavier reste au jeu (focus sur la vue). MouseFreeCooldown = FMath::Max(0.f, MouseFreeCooldown - DeltaTime); const UGameInstance* GI = GetGameInstance(); UGameViewportClient* ViewportClient = GI ? GI->GetGameViewportClient() : nullptr; UPlayerInput* Input = PC->PlayerInput; if (!ViewportClient || !Input || Input->IsPressed(EKeys::LeftMouseButton) || Input->IsPressed(EKeys::RightMouseButton) || Input->IsPressed(EKeys::MiddleMouseButton)) { return; } // VAL B2p-J D2 : verrou aussi quand la capture n'a lieu qu'a l'appui mais cache le curseur (haute precision a l'appui, // SceneViewport.cpp:756-783), etat vu au journal apres le premier relacher. const EMouseCaptureMode Capture = ViewportClient->GetMouseCaptureMode(); const bool bPermanent = Capture == EMouseCaptureMode::CapturePermanently || Capture == EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown; const bool bHiddenOnPress = ViewportClient->HideCursorDuringCapture(); if (!bPermanent && !bHiddenOnPress) { return; } bMouseFreed = true; ++MouseFreeCount; FInputModeGameAndUI FreeMouse; FreeMouse.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); FreeMouse.SetHideCursorDuringCapture(false); PC->SetInputMode(FreeMouse); if (MouseFreeCooldown <= 0.f) { MouseFreeCooldown = 2.f; UE_LOG(LogLifePlay, Log, TEXT("Sexe : souris liberee pour le glisser (le PC l'avait verrouillee, capture %d, curseur cache a l'appui %d ; %d fois dans cette scene)"), static_cast(Capture), bHiddenOnPress ? 1 : 0, MouseFreeCount); } } void ULPCameraModeSubsystem::BeginSexScene() { MappingsAtSexStart = LPCameraModeLocal::MappingSignature(); bMouseFreed = false; MouseFreeCount = 0; MouseFreeCooldown = 0.f; // VAL B2p-J D3/D4 : etat de la souris d'avant la scene, rendu tel quel a la fin (et non un GameOnly impose). const UGameInstance* GI = GetGameInstance(); const UGameViewportClient* ViewportClient = GI ? GI->GetGameViewportClient() : nullptr; bSavedMouseState = ViewportClient != nullptr; if (ViewportClient) { SavedCaptureMode = static_cast(ViewportClient->GetMouseCaptureMode()); SavedLockMode = static_cast(ViewportClient->GetMouseLockMode()); bSavedHideCursor = ViewportClient->HideCursorDuringCapture(); } UE_LOG(LogLifePlay, Log, TEXT("Sexe : debut de scene, touches %s ; souris capture %d verrou %d curseur cache a l'appui %d"), *MappingsAtSexStart, static_cast(SavedCaptureMode), static_cast(SavedLockMode), bSavedHideCursor ? 1 : 0); } void ULPCameraModeSubsystem::EndSexScene() { // Chaque etat que CAM a pu changer pendant la scene est rendu ; une ligne de temoin le confirme. ReleaseCoupleGrab(); bMiddleWasDown = false; bHelpBoxOpen = false; if (HelpRoot.IsValid()) { HelpRoot->SetVisibility(EVisibility::Collapsed); } if (UButton* HelpIcon = HelpButton.Get()) { HelpIcon->SetVisibility(ESlateVisibility::Collapsed); } // D3 (VAL B2p-I) : les 4 lignes retrouvent leur opacite. FString LinesText = TEXT("non touche"); if (UTextBlock* Lines = FadedHelpLines.Get()) { Lines->SetRenderOpacity(FadedLinesOpacity); LinesText = FString::Printf(TEXT("opacite %.2f rendue"), FadedLinesOpacity); } FadedHelpLines.Reset(); UGameInstance* GI = GetGameInstance(); const UWorld* World = GI ? GI->GetWorld() : nullptr; APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const bool bButtonHeld = PC && PC->PlayerInput && (PC->PlayerInput->IsPressed(EKeys::LeftMouseButton) || PC->PlayerInput->IsPressed(EKeys::RightMouseButton) || PC->PlayerInput->IsPressed(EKeys::MiddleMouseButton) || PC->PlayerInput->IsPressed(EKeys::LeftAlt) || PC->PlayerInput->IsPressed(EKeys::RightAlt)); // MoveCamera : CAM l'a mis a faux pendant les prises ; au repos il est faux (le PC le remet au relacher). if (PC && !bButtonHeld && LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCMoveCamera)) { LPCameraModeLocal::WriteBool(PC, LPCameraModeLocal::PCMoveCamera, false); } if (PC && LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCSexC3)) { LPCameraModeLocal::WriteBool(PC, LPCameraModeLocal::PCSexC3, false); } // Souris : rendue a l'etat d'avant la scene (VAL B2p-J D3/D4) seulement si CAM l'avait liberee, que la vue est // toujours dans l'etat pose par CAM et qu'aucun menu n'est a l'ecran (sinon le jeu a deja pose son propre mode). FString InputText = TEXT("non touchee"); if (bMouseFreed && bSavedMouseState) { UGameViewportClient* ViewportClient = GI ? GI->GetGameViewportClient() : nullptr; const bool bStillFreed = ViewportClient && ViewportClient->GetMouseCaptureMode() == EMouseCaptureMode::CaptureDuringMouseDown && ViewportClient->GetMouseLockMode() == EMouseLockMode::DoNotLock && !ViewportClient->HideCursorDuringCapture(); if (PC && bStillFreed && !LPCameraModeLocal::MenuOnScreen(GI)) { const EMouseCaptureMode CaptureBefore = static_cast(SavedCaptureMode); if (CaptureBefore == EMouseCaptureMode::CapturePermanently || CaptureBefore == EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown) { // Capture permanente : GameOnly (haute precision et verrou passent par les operations Slate de SetInputMode). FInputModeGameOnly GameOnly; GameOnly.SetConsumeCaptureMouseDown(CaptureBefore == EMouseCaptureMode::CapturePermanently); PC->SetInputMode(GameOnly); } ViewportClient->SetMouseCaptureMode(CaptureBefore); ViewportClient->SetMouseLockMode(static_cast(SavedLockMode)); ViewportClient->SetHideCursorDuringCapture(bSavedHideCursor); InputText = FString::Printf(TEXT("rendue (capture %d verrou %d curseur cache %d)"), static_cast(SavedCaptureMode), static_cast(SavedLockMode), bSavedHideCursor ? 1 : 0); } else { InputText = bStillFreed ? TEXT("laissee au menu ouvert") : TEXT("deja reposee par le jeu"); } } bMouseFreed = false; const FString MappingsNow = LPCameraModeLocal::MappingSignature(); UE_LOG(LogLifePlay, Log, TEXT("Sexe : fin de scene, etats rendus : prise=%d MoveCamera=%d SexC3=%d ; mode d'entree %s ; 4 lignes %s ; icone et aide repliees ; touches %s (%s)"), CoupleGrabMode, PC && LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCMoveCamera) ? 1 : 0, PC && LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCSexC3) ? 1 : 0, *InputText, *LinesText, MappingsNow == MappingsAtSexStart ? TEXT("inchangees") : TEXT("CHANGEES"), *MappingsNow); } void ULPCameraModeSubsystem::ReleaseCoupleGrab() { CoupleGrabMode = 0; GrabCamera.Reset(); bLeftWasDown = false; bRightWasDown = false; } bool ULPCameraModeSubsystem::TickCoupleMouse(UGameInstance* GI, APlayerController* PC, AActor* View, const UObject* Room, const TArray& Couple, const FVector& Pivot) { UPlayerInput* Input = PC->PlayerInput; const bool bLeft = Input && Input->IsPressed(EKeys::LeftMouseButton); const bool bRight = Input && Input->IsPressed(EKeys::RightMouseButton); const bool bLeftPressed = bLeft && !bLeftWasDown; const bool bRightPressed = bRight && !bRightWasDown; bLeftWasDown = bLeft; bRightWasDown = bRight; const UWorld* World = GI->GetWorld(); // Mode Scene seulement : la vue n'est pas une des personnes du couple (POV) et le PC n'est pas en POV. const bool bScene = Couple.Num() > 0 && !Couple.Contains(View) && !LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCPOVOn); if (!World || !bScene) { CoupleGrabMode = 0; GrabCamera.Reset(); return false; } // Traces : le couple, la camera en vue, CamRoom et le panneau Wall sont ignores. FCollisionQueryParams Query(SCENE_QUERY_STAT(LPCoupleDrag), true); Query.AddIgnoredActors(Couple); Query.AddIgnoredActor(View); if (AActor* Wall = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIWall))) { Query.AddIgnoredActor(Wall); } if (AActor* CamRoom = Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomCamRoom))) { Query.AddIgnoredActor(CamRoom); } // Point de l'ecran vise (curseur de la prise, voir GrabCursor) : premier impact Visibility. auto TraceScreen = [&](const FVector2D& Screen, FVector& Out) -> bool { FVector Origin; FVector Direction; FHitResult Hit; if (!PC->DeprojectScreenPositionToWorld(static_cast(Screen.X), static_cast(Screen.Y), Origin, Direction) || !World->LineTraceSingleByChannel(Hit, Origin, Origin + Direction * LPCameraModeLocal::CursorTraceLength, ECC_Visibility, Query)) { return false; } Out = Hit.ImpactPoint; return true; }; // E2 (CAM-B2p-E) : le sol de Room.Box ne bloque pas toujours le trace (COMMUN-9 : surface trouvee 88 cm sous le // plancher). Dans les limites XY du sol, la surface n'est jamais plus basse que le dessus du sol. // G-e (CAM-B2p-H) : canal Pawn, celui que LPShowCouple met en Block sur les meubles pendant la scene (le canal // Visibility ne voyait pas le lit, COMMUN-11). OutSurface : composant touche au-dessus du sol, nul pour le sol. const UPrimitiveComponent* FloorComponent = Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomFloor)); const AActor* RoomActor = Cast(Room); const bool bFloorKnown = IsValid(FloorComponent) && !(RoomActor && RoomActor->IsHidden()); const FBox FloorBox = bFloorKnown ? FloorComponent->Bounds.GetBox() : FBox(ForceInit); auto TraceDown = [&](const FVector& At, double& OutZ, const UPrimitiveComponent*& OutSurface) -> bool { FHitResult Hit; const FVector Start(At.X, At.Y, Pivot.Z + LPCameraModeLocal::CoupleTraceUp); const FVector End(At.X, At.Y, Pivot.Z - LPCameraModeLocal::CoupleTraceDown); const bool bHit = World->LineTraceSingleByChannel(Hit, Start, End, ECC_Pawn, Query); const bool bOverFloor = bFloorKnown && At.X >= FloorBox.Min.X && At.X <= FloorBox.Max.X && At.Y >= FloorBox.Min.Y && At.Y <= FloorBox.Max.Y; if (!bHit && !bOverFloor) { return false; } OutZ = bHit ? Hit.ImpactPoint.Z : FloorBox.Max.Z; OutSurface = bHit ? Hit.GetComponent() : nullptr; if (bOverFloor && OutZ <= FloorBox.Max.Z) { OutZ = FloorBox.Max.Z; OutSurface = nullptr; } if (OutSurface == FloorComponent) { OutSurface = nullptr; } return true; }; float MouseX = 0.f; float MouseY = 0.f; const bool bMouse = PC->GetMousePosition(MouseX, MouseY); if (CoupleGrabMode == 0) { // Maj tenu : la voie d'origine (Maj + bouton + souris, Room) garde la main, aucune prise. const bool bShiftHeld = Input && (Input->IsPressed(EKeys::LeftShift) || Input->IsPressed(EKeys::RightShift)); if ((!bLeftPressed && !bRightPressed) || bShiftHeld) { return false; } if (!bMouse) { if (!bCursorWarned) { bCursorWarned = true; UE_LOG(LogLifePlay, Log, TEXT("Sexe : curseur sans position, prise du couple indisponible (vue %s)"), *GetNameSafe(View)); } return false; } // Selection : curseur a moins de CoupleSelectRadius px du segment bassin-tete projete d'une des personnes. const FVector MousePoint(MouseX, MouseY, 0.f); bool bOnCouple = false; double Nearest = -1.0; for (const AActor* Person : Couple) { const ACharacter* Character = Cast(Person); const USkeletalMeshComponent* Mesh = Character ? Character->GetMesh() : nullptr; const FVector Low = Mesh ? Mesh->GetSocketLocation(LPCameraModeLocal::PelvisSocket) : Person->GetActorLocation(); const FVector High = Mesh ? Mesh->GetSocketLocation(LPCameraModeLocal::HeadSocket) : Person->GetActorLocation(); FVector2D LowScreen; FVector2D HighScreen; if (PC->ProjectWorldLocationToScreen(Low, LowScreen, false) && PC->ProjectWorldLocationToScreen(High, HighScreen, false)) { const double ScreenGap = FMath::PointDistToSegment(MousePoint, FVector(LowScreen, 0.f), FVector(HighScreen, 0.f)); Nearest = Nearest < 0.0 ? ScreenGap : FMath::Min(Nearest, ScreenGap); if (ScreenGap <= LPCameraModeLocal::CoupleSelectRadius) { bOnCouple = true; break; } } } if (!bOnCouple) { // COMMUN-12 : refus muet rendu visible (au plus une ligne toutes les 2 s) ; le clic reste a la camera. const double Now = FPlatformTime::Seconds(); if (Now - SelectMissLogTime > 2.0) { SelectMissLogTime = Now; UE_LOG(LogLifePlay, Log, TEXT("Sexe : clic hors du couple, pas de prise (curseur (%.0f, %.0f), couple a %s px, seuil %.0f) ; la camera tourne"), MouseX, MouseY, Nearest < 0.0 ? TEXT("?") : *FString::Printf(TEXT("%.0f"), Nearest), LPCameraModeLocal::CoupleSelectRadius); } return false; } CoupleGrabMode = bLeftPressed ? 1 : 2; bGrabMoved = false; LastSpotBlocker.Reset(); LastSurfaceRefused.Reset(); GrabMouseStart = FVector2D(MouseX, MouseY); GrabCursor = GrabMouseStart; GrabLastReal = GrabMouseStart; GrabRawFrames = 0; bGrabAimWarned = false; GrabPivotAfterCam = Pivot; GrabLastMouseX = MouseX; GrabTurned = 0.f; GrabCamera = View; GrabCameraLocation = View->GetActorLocation(); GrabCameraRotation = View->GetActorRotation(); GrabLeadStart = Couple[0]->GetActorLocation(); // G-e : ecart reel entre la racine du maillage la plus basse et la surface sous le couple (journal seulement). double SurfaceZ = 0.0; const UPrimitiveComponent* GrabSurface = nullptr; bGrabHasHeight = TraceDown(Pivot, SurfaceZ, GrabSurface); GrabHeight = bGrabHasHeight ? LPCameraModeLocal::CoupleLowestRoot(Couple) - SurfaceZ : 0.0; FVector Aim; GrabOffset = TraceScreen(GrabMouseStart, Aim) ? FVector(Pivot.X - Aim.X, Pivot.Y - Aim.Y, 0.0) : FVector::ZeroVector; UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple pris (%s) a (%.0f, %.0f, %.0f), racine a %s de la surface %s"), CoupleGrabMode == 1 ? TEXT("deplacement") : TEXT("rotation"), Pivot.X, Pivot.Y, Pivot.Z, bGrabHasHeight ? *FString::Printf(TEXT("%+.0f cm"), GrabHeight) : TEXT("? cm"), !bGrabHasHeight ? TEXT("inconnue") : GrabSurface ? *LPCameraModeLocal::DescribeComponent(GrabSurface) : TEXT("sol")); } const bool bHeld = CoupleGrabMode == 1 ? bLeft : bRight; if (!bHeld) { const FRotator Rotation = Couple[0]->GetActorRotation(); const double CursorTravel = FVector2D::Distance(GrabMouseStart, GrabCursor); if (CoupleGrabMode == 1) { UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple pose a (%.0f, %.0f, %.0f), lacet %.0f"), Pivot.X, Pivot.Y, Pivot.Z, Rotation.Yaw); if (!bGrabMoved) { // COMMUN-12 : glisser sans effet rendu visible (curseur reste sous le seuil, ou souris verrouillee). UE_LOG(LogLifePlay, Log, TEXT("Sexe : glisser sans effet, curseur deplace de %.0f px (seuil %.0f), deltas bruts sur %d images, curseur %s"), CursorTravel, LPCameraModeLocal::CoupleDragThreshold, GrabRawFrames, bMouse ? TEXT("avec position") : TEXT("sans position")); } } else { UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple tourne de %+.0f degres, lacet %.0f"), GrabTurned, Rotation.Yaw); if (FMath::Abs(GrabTurned) < 0.5f) { UE_LOG(LogLifePlay, Log, TEXT("Sexe : rotation sans effet, curseur deplace de %.0f px, deltas bruts sur %d images, curseur %s"), CursorTravel, GrabRawFrames, bMouse ? TEXT("avec position") : TEXT("sans position")); } } // G3 (STE) : une seule fois par pose, si le meneur a ete deplace par la prise (glisser, ou rotation autour d'un pivot // hors du meneur) : le resolveur de LPShowCouple reprend sa place sure ici au lieu de balayer depuis l'ancienne. const double LeadMoved = FVector::Dist(Couple[0]->GetActorLocation(), GrabLeadStart); if (LeadMoved > 1.0) { ULPShowCoupleSubsystem* ShowCouple = GI->GetSubsystem(); // Temoin cote appelant (une ligne par appel) : KeepTogether remet deja LastSafe a chaque image, la ligne // « place reprise » de STE n'apparait donc que si la place a change depuis son dernier passage. UE_LOG(LogLifePlay, Log, TEXT("Sexe : G3 NotifyCoupleTeleported %s (meneur deplace de %.0f cm par la prise)"), ShowCouple ? TEXT("appele") : TEXT("non appele, sous-systeme absent"), LeadMoved); if (ShowCouple) { ShowCouple->NotifyCoupleTeleported(); } } bGrabJustEnded = true; CoupleGrabMode = 0; GrabCamera.Reset(); return false; } // COMMUN-12 : un deplacement du couple pendant la prise qui ne vient pas de CAM (molette de Room, LPShowCouple, jeu) // est ecrit (au plus une ligne par seconde) ; il etait muet (montee de +22 cm par prise a 12:38). if (FVector::Dist(Pivot, GrabPivotAfterCam) > 1.0) { const double Now = FPlatformTime::Seconds(); if (Now - ExternalMoveLogTime > 1.0) { ExternalMoveLogTime = Now; const FVector Outside = Pivot - GrabPivotAfterCam; UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple deplace pendant la prise hors CAM de (%+.0f, %+.0f, %+.0f) ; molette=%+.1f Maj=%d"), Outside.X, Outside.Y, Outside.Z, LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisScroll), Input && (Input->IsPressed(EKeys::LeftShift) || Input->IsPressed(EKeys::RightShift)) ? 1 : 0); } GrabPivotAfterCam = Pivot; } // COMMUN-12 : curseur de la prise. Position reelle quand elle bouge ; sinon (souris verrouillee par le PC, curseur // fige) deltas bruts de la souris : glisser et rotation ne dependent plus du mode d'entree. const FVector2D RealCursor(MouseX, MouseY); // Valeur brute (VAL B2p-J D1) : GetKeyValue applique la sensibilite du projet (0,07), deltas 14 fois trop petits. const FVector2D RawDelta = Input ? FVector2D(Input->GetRawKeyValue(EKeys::MouseX), -Input->GetRawKeyValue(EKeys::MouseY)) : FVector2D::ZeroVector; if (bMouse && !RealCursor.Equals(GrabLastReal, 0.5)) { GrabCursor = RealCursor; } else if (!RawDelta.IsNearlyZero()) { GrabCursor += RawDelta; ++GrabRawFrames; } if (bMouse) { GrabLastReal = RealCursor; } if (CoupleGrabMode == 1) { bGrabMoved |= FVector2D::Distance(GrabMouseStart, GrabCursor) >= LPCameraModeLocal::CoupleDragThreshold; FVector Aim; const bool bAim = bGrabMoved && TraceScreen(GrabCursor, Aim); if (bGrabMoved && !bAim && !bGrabAimWarned) { bGrabAimWarned = true; UE_LOG(LogLifePlay, Log, TEXT("Sexe : glisser, le curseur (%.0f, %.0f) ne vise aucune surface"), GrabCursor.X, GrabCursor.Y); } if (bAim) { FVector Target(Aim.X + GrabOffset.X, Aim.Y + GrabOffset.Y, Pivot.Z); // F3 : meme interieur que E1 (murs reels, marge = demi-boite du couple). LPCameraModeLocal::FRoomInterior Interior; if (bFloorKnown && LPCameraModeLocal::ComputeRoomInterior(RoomActor, Interior)) { const FBox2D& Area = Interior.Area; const double Margin = LPCameraModeLocal::CoupleWallMargin; if (Area.Max.X - Area.Min.X > 2.0 * Margin && Area.Max.Y - Area.Min.Y > 2.0 * Margin) { Target.X = FMath::Clamp(Target.X, Area.Min.X + Margin, Area.Max.X - Margin); Target.Y = FMath::Clamp(Target.Y, Area.Min.Y + Margin, Area.Max.Y - Margin); } } // G-e : la racine du maillage la plus basse est posee sur la surface sous la nouvelle place (dessus du lit ou // sol), plus 3 cm ; elle ne depend plus de l'ecart pris a la selection (un couple qui flottait retombe). double SurfaceZ = 0.0; const UPrimitiveComponent* Surface = nullptr; const bool bSurface = TraceDown(Target, SurfaceZ, Surface); if (bSurface) { Target.Z = Pivot.Z + (SurfaceZ + LPCameraModeLocal::CoupleRootAboveSurface - LPCameraModeLocal::CoupleLowestRoot(Couple)); } // G1 : place libre seulement (boite du couple sans meuble ni mur, lits admis). Sinon le couple reste a la // derniere place libre ; une prise qui part d'une place encombree peut aller vers toute place libre. const ACharacter* LeadCharacter = Cast(Couple[0]); const USkeletalMeshComponent* LeadMesh = LeadCharacter ? LeadCharacter->GetMesh() : nullptr; const double RootOffset = LeadMesh ? LeadMesh->GetComponentLocation().Z - Couple[0]->GetActorLocation().Z : 0.0; FString Blocker; if (bFloorKnown && bSurface && Surface && !LPCameraModeLocal::IsAdmittedBed(Surface)) { // G-f (CAM-B2p-H) : dans une piece, seuls le sol et les lits portent le couple (commode, bureau, armoire, // personne : refuses). Le couple reste a la derniere place admise ; une ligne par changement. const FString Refused = LPCameraModeLocal::DescribeComponent(Surface); if (Refused != LastSurfaceRefused) { LastSurfaceRefused = Refused; UE_LOG(LogLifePlay, Log, TEXT("Sexe : surface refusee %s"), *Refused); } } else if (LPCameraModeLocal::CoupleSpotFree(World, RoomActor, bFloorKnown ? FloorComponent : nullptr, Couple, FVector(Target.X, Target.Y, Target.Z + RootOffset), Couple[0]->GetActorRotation().Yaw, Blocker)) { const FVector Delta = Target - Pivot; for (AActor* Person : Couple) { Person->SetActorLocation(Person->GetActorLocation() + Delta); } GrabPivotAfterCam = Pivot + Delta; // VAL B2p-H N2 : place admise, un nouveau passage sur la meme surface refusee ecrit de nouveau sa ligne. LastSurfaceRefused.Reset(); } else if (Blocker != LastSpotBlocker) { LastSpotBlocker = Blocker; UE_LOG(LogLifePlay, Log, TEXT("Sexe : place refusee, recouvre %s"), *Blocker); } } } else if (CoupleGrabMode == 2) { const float Turn = static_cast(GrabCursor.X - GrabLastMouseX) * LPCameraModeLocal::CoupleTurnPerPixel; GrabLastMouseX = static_cast(GrabCursor.X); if (!FMath::IsNearlyZero(Turn)) { GrabTurned += Turn; for (AActor* Person : Couple) { const FVector Offset = (Person->GetActorLocation() - Pivot).RotateAngleAxis(Turn, FVector::UpVector); FRotator Rotation = Person->GetActorRotation(); Rotation.Yaw += Turn; Person->SetActorLocationAndRotation(Pivot + Offset, Rotation); } } } // Camera figee pendant la prise : le meme clic lance StartMoveCamera (orbite), la molette passe a Room (zoom). if (AActor* Camera = GrabCamera.Get()) { Camera->SetActorLocationAndRotation(GrabCameraLocation, GrabCameraRotation); } LPCameraModeLocal::WriteBool(PC, LPCameraModeLocal::PCMoveCamera, false); return true; } void ULPCameraModeSubsystem::LogSwitchKey(const APlayerController* PC) const { const UGameInstance* GI = GetGameInstance(); const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom); const UObject* Room = IsValid(RoomObject) ? RoomObject : nullptr; const AActor* View = PC ? PC->GetViewTarget() : nullptr; UGameViewportClient* ViewportClient = GI ? GI->GetGameViewportClient() : nullptr; // IgnoreInput() n'est pas const auto Flag = [](const UObject* Container, FName Name) { return LPCameraModeLocal::ReadBool(Container, Name) ? 1 : 0; }; // CAM-B2r : drapeaux qui coupent la souris (boutons ignores si JournalOn, ContactsOn ou MainMenuOn) et MoveCamera du PC. UE_LOG(LogLifePlay, Log, TEXT("Touche C : BuilderOn=%d SpaceOn=%d SexOn=%d CamC1On=%d CamC3On=%d C1Hidden=%d ; entree PC=%d regard ignore=%d viewport ignore=%d ; vue %s (%s) ; Room %s ; PC %s ; indicateur '%s' ; MainMenuOn=%d JournalOn=%d ContactsOn=%d CharGenOn=%d MoveCamera=%d"), Flag(GI, LPCameraModeLocal::GIBuilderOn), Flag(GI, LPCameraModeLocal::GISpaceOn), Flag(GI, LPCameraModeLocal::GISexOn), Flag(Room, LPCameraModeLocal::RoomCamC1On), Flag(Room, LPCameraModeLocal::RoomCamC3On), Flag(Room, LPCameraModeLocal::RoomC1Hidden), PC && PC->InputEnabled() ? 1 : 0, PC && PC->IsLookInputIgnored() ? 1 : 0, ViewportClient && ViewportClient->IgnoreInput() ? 1 : 0, *GetNameSafe(View), View ? *View->GetClass()->GetName() : TEXT("-"), *GetNameSafe(Room), *GetNameSafe(PC), *ShownLabel, Flag(GI, LPCameraModeLocal::GIMainMenuOn), Flag(GI, LPCameraModeLocal::GIJournalOn), Flag(GI, LPCameraModeLocal::GIContactsOn), Flag(GI, LPCameraModeLocal::GICharGenOn), Flag(PC, LPCameraModeLocal::PCMoveCamera)); } void ULPCameraModeSubsystem::WitnessOcclusion(float DeltaTime) { OcclusionCooldown = FMath::Max(0.f, OcclusionCooldown - DeltaTime); if (OcclusionCooldown > 0.f) { return; } OcclusionCooldown = LPCameraModeLocal::OcclusionInterval; const UGameInstance* GI = GetGameInstance(); UWorld* World = GI ? GI->GetWorld() : nullptr; AActor* Room = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom)); ACharacter* Player = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter)); AActor* Cam = IsValid(Room) ? Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomCamRoom)) : nullptr; const USkeletalMeshComponent* Mesh = IsValid(Player) ? Player->GetMesh() : nullptr; if (!World || !IsValid(Cam) || !Mesh) { return; } const FVector Head = Mesh->GetSocketLocation(LPCameraModeLocal::HeadSocket); const FVector CamLoc = Cam->GetActorLocation(); // Trace complexe (polygones, pas les boites englobantes), Room compris : les meubles comptent, le panneau Wall non. FCollisionQueryParams Query(SCENE_QUERY_STAT(LPCameraOcclusion), true); Query.AddIgnoredActor(Player); Query.AddIgnoredActor(Cam); if (AActor* Wall = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIWall))) { Query.AddIgnoredActor(Wall); } FHitResult Hit; if (!World->LineTraceSingleByChannel(Hit, CamLoc, Head, ECC_Visibility, Query)) { return; } const UPrimitiveComponent* Comp = Hit.GetComponent(); const UStaticMeshComponent* MeshComp = Cast(Comp); OcclusionCooldown = LPCameraModeLocal::OcclusionLogInterval; UE_LOG(LogLifePlay, Log, TEXT("Temoin C2 : joueur masque par %s (%s) de %s, visible=%d, a %.0f cm de la camera (camera a %.0f cm de la tete)"), *GetNameSafe(Comp), MeshComp ? *GetNameSafe(MeshComp->GetStaticMesh()) : TEXT("-"), *GetNameSafe(Hit.GetActor()), Comp && Comp->IsVisible() ? 1 : 0, Hit.Distance, FVector::Dist(CamLoc, Head)); } void ULPCameraModeSubsystem::WitnessC2(float DeltaTime) { WitnessCooldown = FMath::Max(0.f, WitnessCooldown - DeltaTime); if (WitnessCooldown > 0.f) { return; } const UGameInstance* GI = GetGameInstance(); UWorld* World = GI ? GI->GetWorld() : nullptr; AActor* Room = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom)); ACharacter* Player = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter)); AActor* Cam = IsValid(Room) ? Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomCamRoom)) : nullptr; const USkeletalMeshComponent* Mesh = IsValid(Player) ? Player->GetMesh() : nullptr; if (!World || !IsValid(Cam) || !Mesh) { return; } const FVector Head = Mesh->GetSocketLocation(LPCameraModeLocal::HeadSocket); const FVector CamLoc = Cam->GetActorLocation(); const float Distance = FVector::Dist(Head, CamLoc); if (Distance >= LPCameraModeLocal::WitnessNearDistance) { return; } WitnessCooldown = LPCameraModeLocal::WitnessInterval; // Balayage Camera a plat, de la tete vers la camera (ou vers le dos si elle est au-dessus de la tete). FVector Dir = CamLoc - Head; Dir.Z = 0.f; if (!Dir.Normalize()) { Dir = -Player->GetActorForwardVector(); } FCollisionQueryParams Query(SCENE_QUERY_STAT(LPCameraWitness), false); Query.AddIgnoredActor(Room); Query.AddIgnoredActor(Player); Query.AddIgnoredActor(Cam); // Meme liste que MakeArray_5 de Room apres B2g. if (AActor* Wall = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIWall))) { Query.AddIgnoredActor(Wall); } // Canal Camera (sonde de Room jusqu'a B2e) et Visibility (sonde de Room depuis B2f). auto Probe = [&](ECollisionChannel Channel, const FVector& Direction) -> FString { FHitResult Hit; if (!World->SweepSingleByChannel(Hit, Head, Head + Direction * LPCameraModeLocal::WitnessProbeLength, FQuat::Identity, Channel, FCollisionShape::MakeSphere(LPCameraModeLocal::WitnessProbeRadius), Query)) { return TEXT("rien"); } const AActor* Owner = Hit.GetActor(); return FString::Printf(TEXT("%s de %s (cache=%d) a %.0f cm%s"), *GetNameSafe(Hit.GetComponent()), *GetNameSafe(Owner), Owner && Owner->IsHidden() ? 1 : 0, Hit.Distance, Hit.bStartPenetrating ? TEXT(", depart deja dedans") : TEXT("")); }; const FString CameraText = Probe(ECC_Camera, Dir); const FString VisibilityText = Probe(ECC_Visibility, Dir); // Axe de la sonde 459 de Room : a plat, derriere la rotation de controle du PC. const APlayerController* PC = World->GetFirstPlayerController(); const FVector Back = PC ? -FRotationMatrix(FRotator(0.f, PC->GetControlRotation().Yaw, 0.f)).GetUnitAxis(EAxis::X) : -Player->GetActorForwardVector(); const FString BackText = Probe(ECC_Visibility, Back); FVector Target; const FString TargetText = LPCameraModeLocal::ReadVector(Room, LPCameraModeLocal::RoomC2Target, Target) ? FString::Printf(TEXT("%.0f cm"), FVector::Dist(Head, Target)) : FString(TEXT("illisible")); const FVector Offset = Head - Player->GetActorLocation(); // Marge de la camera au bord le plus proche de la boite C2 (X et Y), pour separer boite et sonde. FString BoxText(TEXT("illisible")); FVector BoxMin; FVector BoxMax; if (LPCameraModeLocal::ReadVector(Room, LPCameraModeLocal::RoomC2BoxMin, BoxMin) && LPCameraModeLocal::ReadVector(Room, LPCameraModeLocal::RoomC2BoxMax, BoxMax)) { const double Margin = FMath::Min(FMath::Min(CamLoc.X - BoxMin.X, BoxMax.X - CamLoc.X), FMath::Min(CamLoc.Y - BoxMin.Y, BoxMax.Y - CamLoc.Y)); BoxText = FString::Printf(TEXT("marge %.0f cm (largeur X %.0f, Y %.0f)"), Margin, BoxMax.X - BoxMin.X, BoxMax.Y - BoxMin.Y); } UE_LOG(LogLifePlay, Log, TEXT("Temoin C2 : camera a %.0f cm de la tete (hauteur %+.0f) ; cible C2 a %s ; tete (%.0f, %.0f), decalage capsule %.0f cm ; vers la camera, Camera : %s, Visibility : %s ; vers l'arriere, Visibility : %s ; boite C2 : %s"), Distance, CamLoc.Z - Head.Z, *TargetText, Head.X, Head.Y, Offset.Size2D(), *CameraText, *VisibilityText, *BackText, *BoxText); } FString ULPCameraModeSubsystem::ComputeLabel() const { const UGameInstance* GI = GetGameInstance(); const UWorld* World = GI ? GI->GetWorld() : nullptr; APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; if (!PC) { PC = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPC)); } const AActor* View = IsValid(PC) ? PC->GetViewTarget() : nullptr; if (!IsValid(View)) { return FString(); } // Ordre du paragraphe 2 de la SPEC ; chaque comparaison exige une reference non nulle. const UObject* Birdview = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIBirdview); if ((Birdview && View == Birdview) || View->GetClass()->GetName().Contains(TEXT("Birdview"))) { return TEXT("Camera: Overview"); } if (LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn)) { if (LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCPOVOn)) { return TEXT("Camera: POV"); } return LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCSexC3) ? TEXT("Camera: C3 Couple") : TEXT("Camera: Scene"); } const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom); const UObject* Room = IsValid(RoomObject) ? RoomObject : nullptr; const UObject* PlayerCharacter = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter); if (PlayerCharacter && View == PlayerCharacter && LPCameraModeLocal::ReadBool(Room, LPCameraModeLocal::RoomCamC1On)) { return TEXT("Camera: C1 First person"); } const bool bC3 = LPCameraModeLocal::ReadBool(Room, LPCameraModeLocal::RoomCamC3On); const UObject* CamRoom = LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomCamRoom); if (CamRoom && View == CamRoom) { return bC3 ? TEXT("Camera: C3 Ceiling") : TEXT("Camera: C2 Follow"); } // SceneCamera de Room est ChargenCameras[0] (CAM-AUDIT § 1) : le constructeur et l'ancienne vue C3 se // distinguent du studio par BuilderOn et CamC3On, testes avant le tableau des cameras du studio. const UObject* SceneCamera = LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomSceneCamera); const bool bSceneCamera = SceneCamera && View == SceneCamera; if (bSceneCamera && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GIBuilderOn)) { return TEXT("Camera: Builder"); } if (bSceneCamera && bC3) { return TEXT("Camera: C3 Room (old)"); } if (LPCameraModeLocal::ArrayContains(GI, LPCameraModeLocal::GIChargenCameras, View)) { return TEXT("Camera: Studio"); } return FString(); } bool ULPCameraModeSubsystem::EnsureWidget() { const UGameInstance* GI = GetGameInstance(); UGameViewportClient* ViewportClient = GI ? GI->GetGameViewportClient() : nullptr; if (!ViewportClient || !FSlateApplication::IsInitialized()) { return false; } if (Root.IsValid() && Viewport.Get() == ViewportClient) { return true; } // Premier ajout, ou viewport remplace depuis : retirer de l'ancien s'il existe encore, puis ajouter. RemoveWidget(); TSharedPtr NewText; TSharedRef NewRoot = SNew(SOverlay) .Visibility(EVisibility::Collapsed) + SOverlay::Slot().HAlign(HAlign_Left).VAlign(VAlign_Bottom).Padding(FMargin(16.f)) [ SAssignNew(NewText, STextBlock) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 12)) .ColorAndOpacity(FSlateColor(FLinearColor(1.f, 1.f, 1.f, 0.7f))) .ShadowOffset(FVector2D(1.f, 1.f)) .ShadowColorAndOpacity(FLinearColor(0.f, 0.f, 0.f, 0.7f)) ]; ViewportClient->AddViewportWidgetContent(NewRoot, LPCameraModeLocal::ViewportZOrder); Root = NewRoot; Text = NewText; Viewport = ViewportClient; ShownLabel.Reset(); bShown = false; return true; } void ULPCameraModeSubsystem::RemoveWidget() { if (Root.IsValid()) { if (UGameViewportClient* ViewportClient = Viewport.Get()) { ViewportClient->RemoveViewportWidgetContent(Root.ToSharedRef()); } } Root.Reset(); Text.Reset(); Viewport.Reset(); ShownLabel.Reset(); bShown = false; } void ULPCameraModeSubsystem::SetShown(bool bShow) { if (!Root.IsValid() || bShown == bShow) { return; } // Racine non cliquable quand elle est visible ; repliee sinon. Root->SetVisibility(bShow ? EVisibility::HitTestInvisible : EVisibility::Collapsed); bShown = bShow; } FString ULPCameraModeSubsystem::BuildControlsHelp() const { // Controles de B2p-C a B2p-I (CAM-B2p-I-SPEC § 1.6). Molette : CAM ne la lit pas (bSexWheelZoom faux), mais B2p-R // (Room, O1 : MoveObject OU SexOn) la donne au deplacement du couple pendant le sexe (COMMUN-12, 12:37:57 : // molette=+1.0, couple +9 cm) : elle est citee. Panneau : Alt ou clic molette brefs (COMMUN-12 : Espace rendu au jeu). using LPCameraModeLocal::MappedKeys; return FString::Printf(TEXT( "CAMERA\n" " %s: next view (Scene > C3 couple close-up > each partner's eyes)\n" " %s / %s: move closer / away %s / %s: orbit around the couple (eyes view: look around)\n" " Hold a mouse button away from the couple and move the mouse: rotate the camera\n" "COUPLE\n" " Left-drag the couple: move it (floor or bed only)\n" " Right-drag the couple: turn it\n" " %s, %s: move the couple Mouse wheel: raise / lower the couple\n" "ACTIONS\n" " Alt or middle click (short press): show / hide this panel"), *MappedKeys(LPCameraModeLocal::SwitchCameraAction, 0), *MappedKeys(LPCameraModeLocal::AxisMoveForward, 1), *MappedKeys(LPCameraModeLocal::AxisMoveForward, -1), *MappedKeys(LPCameraModeLocal::AxisMoveRight, -1), *MappedKeys(LPCameraModeLocal::AxisMoveRight, 1), *MappedKeys(LPCameraModeLocal::AxisLeftRight, 0), *MappedKeys(LPCameraModeLocal::AxisDownUp, 0)); } void ULPCameraModeSubsystem::TickControlsHelp() { UGameInstance* GI = GetGameInstance(); const bool bSexOn = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn); const UWorld* World = GI ? GI->GetWorld() : nullptr; const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; // I-d : les 4 lignes d'origine sont rendues invisibles (opacite 0) seulement quand le bloc porte ce texte. La liaison // Get_TextBlock_1_Text reste en place (SetText la retirerait) : un autre texte du meme bloc reste affiche. const UUserWidget* SexUI = bSexOn ? Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GISexInterface)) : nullptr; if (UTextBlock* Lines = SexUI ? Cast(SexUI->GetWidgetFromName(LPCameraModeLocal::SexHelpLinesName)) : nullptr) { const bool bOldHelp = Lines->GetText().ToString().StartsWith(LPCameraModeLocal::SexHelpLinesStart, ESearchCase::CaseSensitive); if (bOldHelp && Lines->GetRenderOpacity() > 0.f) { FadedLinesOpacity = Lines->GetRenderOpacity(); FadedHelpLines = Lines; Lines->SetRenderOpacity(0.f); UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : 4 lignes d'origine masquees (%s)"), *GetPathNameSafe(Lines)); } else if (!bOldHelp && FadedHelpLines.Get() == Lines) { // Le meme bloc porte un autre texte : il retrouve son opacite. Lines->SetRenderOpacity(FadedLinesOpacity); FadedHelpLines.Reset(); UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : TextBlock_1 porte un autre texte, opacite rendue")); } } // I-a : icone inseree une fois par instance du panneau, pendant le sexe ; repliee hors du sexe (le Cursor sert // aussi aux autres menus d'action). UUserWidget* ActionCursor = PC ? Cast(LPCameraModeLocal::ReadObject(PC, LPCameraModeLocal::PCCursorREF)) : nullptr; if (bSexOn && ActionCursor && HelpCursor.Get() != ActionCursor) { HelpCursor = ActionCursor; HelpButton = AttachHelpIcon(ActionCursor); } const FString HelpBody = bSexOn ? BuildControlsHelp() : FString(); if (UButton* HelpIcon = HelpButton.Get()) { const ESlateVisibility IconVisibility = bSexOn ? ESlateVisibility::Visible : ESlateVisibility::Collapsed; if (HelpIcon->GetVisibility() != IconVisibility) { HelpIcon->SetVisibility(IconVisibility); } if (bSexOn && HelpBody != HelpTextShown) { // Remappage (menu Keyboard) : info-bulle et boite suivent. HelpIcon->SetToolTipText(FText::FromString(HelpBody)); } } // I-c : boite d'aide ouverte par le clic, seulement pendant le sexe et panneau ouvert (meme test que le PC : // BelowCursor visible) ; fermee avec le panneau. const UWidget* PanelArea = ActionCursor ? ActionCursor->GetWidgetFromName(LPCameraModeLocal::CursorBelowCursor) : nullptr; const bool bPanelOpen = bSexOn && PanelArea && PanelArea->IsVisible(); if (!bPanelOpen) { bHelpBoxOpen = false; } if (bHelpBoxOpen && EnsureHelpBox()) { if (!HelpText->GetText().ToString().Equals(HelpBody, ESearchCase::CaseSensitive)) { HelpText->SetText(FText::FromString(HelpBody)); } HelpRoot->SetVisibility(EVisibility::HitTestInvisible); } else if (HelpRoot.IsValid()) { HelpRoot->SetVisibility(EVisibility::Collapsed); } if (bSexOn) { HelpTextShown = HelpBody; } } UButton* ULPCameraModeSubsystem::AttachHelpIcon(UUserWidget* ActionCursor) { if (UButton* Existing = Cast(ActionCursor->GetWidgetFromName(LPCameraModeLocal::HelpButtonName))) { Existing->OnClicked.AddUniqueDynamic(this, &ULPCameraModeSubsystem::ToggleControlsHelp); return Existing; } UWidget* SearchBox = ActionCursor->GetWidgetFromName(LPCameraModeLocal::CursorSearchAction); UPanelWidget* SearchParent = SearchBox ? SearchBox->GetParent() : nullptr; if (!SearchBox || !SearchParent || !ActionCursor->WidgetTree) { UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : icone non inseree, %s introuvable dans %s"), !SearchBox ? TEXT("SearchAction") : TEXT("le parent de SearchAction"), *GetNameSafe(ActionCursor)); return nullptr; } const UCanvasPanelSlot* SearchCanvasSlot = Cast(SearchBox->Slot); if (SearchCanvasSlot && (SearchCanvasSlot->GetAnchors().IsStretchedHorizontal() || SearchCanvasSlot->GetAnchors().IsStretchedVertical())) { UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : icone non inseree, SearchAction est ancre en etirement dans %s (%s)"), *SearchParent->GetName(), *SearchParent->GetClass()->GetName()); return nullptr; } UButton* IconButton = ActionCursor->WidgetTree->ConstructWidget(UButton::StaticClass(), LPCameraModeLocal::HelpButtonName); IconButton->SetBackgroundColor(FLinearColor::Transparent); UTexture2D* IconTexture = LoadObject(nullptr, LPCameraModeLocal::HelpIconPath); if (IconTexture) { UImage* IconImage = ActionCursor->WidgetTree->ConstructWidget(UImage::StaticClass(), TEXT("LPControlsHelpIcon")); IconImage->SetBrushFromTexture(IconTexture); IconImage->SetDesiredSizeOverride(FVector2D(LPCameraModeLocal::HelpIconSize, LPCameraModeLocal::HelpIconSize)); IconButton->SetContent(IconImage); } else { // Texture absente : un « i » en texte. UTextBlock* IconLetter = ActionCursor->WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("LPControlsHelpLetter")); IconLetter->SetText(FText::FromString(TEXT("i"))); IconLetter->SetFont(FCoreStyle::GetDefaultFontStyle("Bold", 18)); IconLetter->SetColorAndOpacity(FSlateColor(FLinearColor::White)); IconButton->SetContent(IconLetter); } IconButton->SetToolTipText(FText::FromString(BuildControlsHelp())); IconButton->OnClicked.AddDynamic(this, &ULPCameraModeSubsystem::ToggleControlsHelp); // AddChild, pas InsertChildAt : l'ordre d'insertion ne passe pas a la vue Slate sans reconstruction. L'icone vient // donc en fin de la rangee du filtre (apres ses boutons), ou a droite du filtre dans un CanvasPanel. UPanelSlot* IconSlot = SearchParent->AddChild(IconButton); if (!IconSlot) { // VAL B2p-I D1 : panneau a enfant unique deja occupe (PanelWidget.cpp:139-142) : rien n'est insere. UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : icone non inseree, %s (%s) refuse un enfant de plus"), *SearchParent->GetName(), *SearchParent->GetClass()->GetName()); return nullptr; } if (UHorizontalBoxSlot* RowSlot = Cast(IconSlot)) { RowSlot->SetVerticalAlignment(VAlign_Center); RowSlot->SetPadding(FMargin(4.f, 0.f, 0.f, 0.f)); } else if (UCanvasPanelSlot* CanvasSlot = Cast(IconSlot)) { if (SearchCanvasSlot) { CanvasSlot->SetAnchors(SearchCanvasSlot->GetAnchors()); CanvasSlot->SetAlignment(SearchCanvasSlot->GetAlignment()); CanvasSlot->SetPosition(SearchCanvasSlot->GetPosition() + FVector2D(SearchCanvasSlot->GetSize().X + 4.0, 0.0)); CanvasSlot->SetSize(FVector2D(LPCameraModeLocal::HelpIconSize + 8.0, LPCameraModeLocal::HelpIconSize + 8.0)); CanvasSlot->SetZOrder(SearchCanvasSlot->GetZOrder()); } } UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : icone inseree dans %s (%s, %d enfants), image %s"), *SearchParent->GetName(), *SearchParent->GetClass()->GetName(), SearchParent->GetChildrenCount(), IconTexture ? LPCameraModeLocal::HelpIconPath : TEXT("absente, lettre i")); return IconButton; } void ULPCameraModeSubsystem::ToggleControlsHelp() { bHelpBoxOpen = !bHelpBoxOpen; UE_LOG(LogLifePlay, Log, TEXT("Aide sexe : boite d'aide %s (clic sur l'icone)"), bHelpBoxOpen ? TEXT("ouverte") : TEXT("fermee")); TickControlsHelp(); } bool ULPCameraModeSubsystem::EnsureHelpBox() { const UGameInstance* GI = GetGameInstance(); UGameViewportClient* ViewportClient = GI ? GI->GetGameViewportClient() : nullptr; if (!ViewportClient || !FSlateApplication::IsInitialized()) { return false; } if (HelpRoot.IsValid() && HelpViewport.Get() == ViewportClient) { return true; } RemoveHelpBox(); // En bas a droite, la ou etaient les 4 lignes : fond sombre, texte blanc ombre (lisible sans LPReadableText). TSharedPtr NewHelpText; TSharedRef NewHelpRoot = SNew(SOverlay) .Visibility(EVisibility::Collapsed) + SOverlay::Slot().HAlign(HAlign_Right).VAlign(VAlign_Bottom).Padding(FMargin(24.f, 24.f, 24.f, 96.f)) [ SNew(SBorder) .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) .BorderBackgroundColor(FLinearColor(0.f, 0.f, 0.f, 0.75f)) .Padding(FMargin(12.f, 8.f)) [ SAssignNew(NewHelpText, STextBlock) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 14)) .ColorAndOpacity(FSlateColor(FLinearColor::White)) .ShadowOffset(FVector2D(1.f, 1.f)) .ShadowColorAndOpacity(FLinearColor::Black) ] ]; ViewportClient->AddViewportWidgetContent(NewHelpRoot, LPCameraModeLocal::HelpZOrder); HelpRoot = NewHelpRoot; HelpText = NewHelpText; HelpViewport = ViewportClient; return true; } void ULPCameraModeSubsystem::RemoveHelpBox() { if (HelpRoot.IsValid()) { if (UGameViewportClient* ViewportClient = HelpViewport.Get()) { ViewportClient->RemoveViewportWidgetContent(HelpRoot.ToSharedRef()); } } HelpRoot.Reset(); HelpText.Reset(); HelpViewport.Reset(); }