// Lot CAM-B2c - voir LPCameraModeSubsystem.h #include "LPCameraModeSubsystem.h" #include "Blueprint/UserWidget.h" #include "Blueprint/WidgetTree.h" #include "Camera/PlayerCameraManager.h" #include "LPRoomBoundsCameraModifier.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 // A1 (CAM2-SPEC-01 § 4.1, rev. 4) : tangage de la tete en C1 hors scene intime, a la souris (CursorY). // CursorY est deja mis a l'echelle par la sensibilite 0,07 du mappage (DefaultInput) : 1 unite = 1 degre. const FName AxisCursorY(TEXT("CursorY")); const FName AxisCursorX(TEXT("CursorX")); const float PovWitnessInterval = 1.f; // CAM2-DIAG-02 § 5 : temoin « POV souris », au plus une ligne par seconde const float C1MouseLookScale = 1.f; const float C1PitchMin = -60.f; const float C1PitchMax = 70.f; // CAM2-SPEC-01 rev. 3 § 12.2 (A1-R) : le tangage passe par la rotation de controle, que Room.UpdateRoomCam reprend // (liaison BP 514 -> 539 et 514 -> 515). CAM2-SPEC-02 § 1 (essai COMMUN-17, « souris vers le bas va vers le haut ») : // CursorY est positif quand la souris monte, donc +1 (le POV de PC est corrige du meme sens, broche 109.B). const float C1MouseSign = 1.f; const float C1MouseLogInterval = 1.f; 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) // CAM2-DIAG-02 § 8 (arbitrage de Big Boss) : dehors, plafond de la vue a sol + 1000 cm (WallT masque, aucun plafond trouve). const FName RoomCeilingWallT(TEXT("WallT")); const float OutdoorViewCeilingHeight = 1000.f; 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) // CAM2-SPEC-01 rev. 3 § 12.4 et § 12.8 : bornes de la camera de scene, partagees par le recadrage, la borne de vue // (modificateur de camera) et CameraHeight : une seule paire de valeurs. const float SceneFloorMargin = 30.f; // cm au-dessus du dessus du sol const float SceneCeilingMargin = 20.f; // cm sous le plafond (trouve ou estime) const float SceneWallMargin = 20.f; // cm devant un mur (= RecenterWallMargin) // Liste blanche de la borne de vue : seules les vues de CameraScene (Room.SceneCamera) en scene ou en C3 de piece. const TCHAR* const LabelScene = TEXT("Camera: Scene"); const TCHAR* const LabelC3Couple = TEXT("Camera: C3 Couple"); const TCHAR* const LabelC3RoomOld = TEXT("Camera: C3 Room (old)"); const float ViewBoundsLogInterval = 1.f; // 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; // KEY2 (KEY2-SPEC-01 § 6, § 8b, § 8c) : touche d'action (meme evenement que G-g), hauteur de la camera de scene, // couple sur Y. Actions et axes de DefaultInput.ini, remappables dans le menu Clavier. const FName ActionPanelAction(TEXT("ActionPanel")); const FName AxisCameraHeight(TEXT("CameraHeight")); const FName AxisCoupleMoveY(TEXT("CoupleMoveY")); const float CameraHeightSpeed = 100.f; // cm par seconde (a regler a l'essai) // Hauteur ajoutee bornee comme C2Height de Room ([-40, 150], KEY2-CART-02 § 5), depuis le dernier recadrage. const float CameraHeightMin = -40.f; const float CameraHeightMax = 150.f; // Repere de CoupleMoveY (Q12, decision 14) : World = Y du monde, Couple = droite du premier acteur. Plus lu depuis CAM2-SPEC-02 § 7 // (CoupleMoveY = Z) ; repere des fleches : bCoupleKeysCameraFrame. enum class ECoupleMoveSpace : uint8 { World, Couple }; const ECoupleMoveSpace CoupleMoveSpace = ECoupleMoveSpace::World; const float CoupleMoveYStep = 5.f; // par unite d'axe et par image, comme le X de PC (KEY2-CART-02 § 5) // CAM2-SPEC-02 § 7 (decision de l'utilisateur) : en scene, V / X (axe CoupleMoveY, nom garde, N1) montent et baissent le // couple (Z) ; fleches haut / bas = Y, gauche / droite = X, dans la piece visible ou cachee (PC ne fait plus rien en scene). // Repere monde par defaut ; vrai = repere de la camera (haut = s'eloigner de la camera, droite = droite de la camera). const bool bCoupleKeysCameraFrame = false; const float CoupleKeyUpSign = 1.f; // fleche Haut = +Y (repere monde) const float CoupleKeyRightSign = 1.f; // fleche Droite = +X (repere monde) const float CoupleOutdoorZBelow = 30.f; // piece cachee : Z du pivot au plus 30 cm sous son Z d'entree en scene const float CoupleOutdoorZAbove = 150.f; // piece cachee : et au plus 150 cm au-dessus // CAM2-SPEC-02 § 5 (S1-C++) : studio de creation. Vue des cameras du studio et personnage edite decales du meme vecteur // (-d, 0, 0), sans modifier GI. d = 135 cm : places CharGen de GI.ActorTransforms a X -3131290 et -3131300 (MCP), face +X // du panneau GI.Wall a -3131485, 50 cm de marge : d <= -3131300 + 3131435 = 135. Recentrage lateral (Y) non demande : 0. const FName GICharacterBeingEdited(TEXT("CharacterBeingEdited")); const FName GIActorTransforms(TEXT("ActorTransforms")); const FVector StudioOffset(-135.0, 0.0, 0.0); const double StudioSpotTolerance = 5.0; // cm (XY) : le personnage est sur une place CharGen // 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); } // KEY2 : une touche de l'action Name vient d'etre pressee, modificateurs exacts (comme InteractPressed et // ModifiersMatch de LPDirectControlSubsystem.cpp), mappages et remappages suivis. bool ActionJustPressed(const APlayerController* PC, FName Action) { const UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!PC || !PC->PlayerInput || !Settings) { return false; } const bool bCtrl = PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl); const bool bShift = PC->IsInputKeyDown(EKeys::LeftShift) || PC->IsInputKeyDown(EKeys::RightShift); const bool bAlt = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt); const bool bCmd = PC->IsInputKeyDown(EKeys::LeftCommand) || PC->IsInputKeyDown(EKeys::RightCommand); for (const FInputActionKeyMapping& Map : Settings->GetActionMappings()) { if (Map.ActionName == Action && Map.bCtrl == bCtrl && Map.bShift == bShift && Map.bAlt == bAlt && Map.bCmd == bCmd && PC->PlayerInput->WasJustPressed(Map.Key)) { return true; } } return false; } // 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; // Plafond de la VUE de scene : Ceiling, sauf dehors (CAM2-DIAG-02 § 8, arbitrage de Big Boss) ; le couple garde Ceiling. double ViewCeiling = 0.0; bool bOutdoorSky = false; 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)"); } // Piece d'exterieur : aucun plafond trouve et plafond WallT masque par LPOutdoorRoomSubsystem. Le plafond estime // (sol + 350) ne borne plus la vue, qui monte jusqu'a sol + OutdoorViewCeilingHeight. Out.ViewCeiling = Out.Ceiling; const USceneComponent* CeilingWallT = Out.bCeilingFound ? nullptr : Cast(ReadObject(Room, RoomCeilingWallT)); if (IsValid(CeilingWallT) && CeilingWallT->bHiddenInGame) { Out.bOutdoorSky = true; Out.ViewCeiling = Out.FloorTop + OutdoorViewCeilingHeight; } 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); } static void RemoveViewBounds(UGameInstance* GI); void ULPCameraModeSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(SwitchKeyHandle); FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); RemoveViewBounds(GetGameInstance()); RemoveWidget(); RemoveHelpBox(); Super::Deinitialize(); } // R-B1f (CAM2-SPEC-01 § 9.2) : au front montant de SexOn, C1Hidden repasse a faux. Une scene lancee depuis C1 ne laisse // plus cheveux et cou masques (risque non observe, sans reponse de l'utilisateur ; traite comme non observe). static void ClearC1HiddenOnSexEntry(UGameInstance* GI) { static bool bSexWasOn = false; const bool bSexOn = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn); if (bSexOn && !bSexWasOn) { if (UObject* Room = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom)) { LPCameraModeLocal::WriteBool(Room, LPCameraModeLocal::RoomC1Hidden, false); } } bSexWasOn = bSexOn; } // A1-R (CAM2-SPEC-01 rev. 3 § 12.2, decision de l'utilisateur : « on bouge la tete avec la souris sur tous les axes ») : // en C1 hors scene intime, la souris (CursorY) regle le tangage de la rotation de controle. Room.UpdateRoomCam (chaque image) // et Room.CircleCamera (lacet) reprennent ce tangage (liaisons BP 514 -> 539 et 514 -> 515) : le C++ n'ecrit plus FirstPersonSM, // que UpdateRoomCam reecrivait avec un tangage nul (CAM2-DIAG-01). Appel a chaque image : CursorY est un delta par image. static void TickC1Pitch(UGameInstance* GI) { if (!GI || LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn)) { return; } UWorld* World = GI->GetWorld(); APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; if (!PC) { return; } const float MouseY = PC->GetInputAxisValue(LPCameraModeLocal::AxisCursorY); if (FMath::IsNearlyZero(MouseY)) { return; } const FRotator Control = PC->GetControlRotation(); const double Wanted = FMath::Clamp(FRotator::NormalizeAxis(Control.Pitch) + LPCameraModeLocal::C1MouseSign * MouseY * LPCameraModeLocal::C1MouseLookScale, static_cast(LPCameraModeLocal::C1PitchMin), static_cast(LPCameraModeLocal::C1PitchMax)); PC->SetControlRotation(FRotator(Wanted, Control.Yaw, Control.Roll)); // Temoin C1 souris (§ 12.5, 1) : au plus une ligne par seconde tant que la souris bouge. static double NextLog = 0.0; const double Now = FPlatformTime::Seconds(); if (Now < NextLog) { return; } NextLog = Now + LPCameraModeLocal::C1MouseLogInterval; const ACharacter* Player = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter)); const USceneComponent* Camera = IsValid(Player) ? Cast(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerFirstPersonSM)) : nullptr; const FString ReadPitch = IsValid(Camera) ? FString::Printf(TEXT("%+.1f"), FRotator::NormalizeAxis(Camera->GetComponentRotation().Pitch)) : FString(TEXT("-")); const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom); UE_LOG(LogLifePlay, Log, TEXT("C1 souris : CursorY=%+.2f ; tangage voulu=%+.1f ; tangage de controle=%+.1f ; tangage lu (composant)=%s ; lacet de controle=%+.0f ; minuterie RoomCam active=%d"), MouseY, Wanted, FRotator::NormalizeAxis(PC->GetControlRotation().Pitch), *ReadPitch, Control.Yaw, LPCameraModeLocal::TimerActive(IsValid(RoomObject) ? RoomObject : nullptr, LPCameraModeLocal::RoomCamTimer, World) ? 1 : 0); } // A1-R (§ 12.2) : en quittant C1, le tangage de controle repasse a 0 ; la vue C1 suivante repart a l'horizontale et aucun // autre mode ne garde un tangage herite (PlaceC2 le remet aussi a 0 a la pose de C2). static void ResetC1PitchOnExit(UGameInstance* GI, bool bC1Now) { static bool bC1Was = false; if (bC1Was && !bC1Now) { UWorld* World = GI ? GI->GetWorld() : nullptr; if (APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr) { const FRotator Control = PC->GetControlRotation(); if (!FMath::IsNearlyZero(FRotator::NormalizeAxis(Control.Pitch))) { PC->SetControlRotation(FRotator(0.0, Control.Yaw, Control.Roll)); UE_LOG(LogLifePlay, Log, TEXT("C1 : tangage de controle remis a 0 en sortie de C1 (etait %+.1f)"), FRotator::NormalizeAxis(Control.Pitch)); } } } bC1Was = bC1Now; } // Pivot de la camera de scene : le couple pendant le sexe, sinon le joueur. Faux si aucun n'est connu. static bool ScenePivot(const UGameInstance* GI, FVector& OutPivot) { if (LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn)) { const TArray Couple = LPCameraModeLocal::ReadActors(GI, LPCameraModeLocal::GISexPersons); FVector Sum = FVector::ZeroVector; int32 Count = 0; for (const AActor* Person : Couple) { if (IsValid(Person)) { Sum += Person->GetActorLocation(); ++Count; } } if (Count > 0) { OutPivot = Sum / Count; return true; } } const AActor* Player = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter)); if (IsValid(Player)) { OutPivot = Player->GetActorLocation(); return true; } return false; } // F2-B (CAM2-SPEC-01 rev. 3 § 12.4) : borne de la vue de CameraScene par un modificateur de camera, qui agit dans le calcul de // la vue, apres tous les BP (UpdateRoomCam, orbite a la souris, AutoCamera). Liste blanche : vue = Room.SceneCamera ET libelle // Scene, C3 Couple ou C3 Room (old), hors menu. Jamais en C1, POV, Overview, C2 Follow, C3 Ceiling, Builder, Studio. static void TickViewBounds(UGameInstance* GI, const FString& Label, bool bMenu) { UWorld* World = GI ? GI->GetWorld() : nullptr; APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; APlayerCameraManager* Manager = PC ? PC->PlayerCameraManager.Get() : nullptr; if (!Manager) { return; } ULPRoomBoundsCameraModifier* Modifier = Cast(Manager->FindCameraModifierByClass(ULPRoomBoundsCameraModifier::StaticClass())); if (!Modifier) { Modifier = Cast(Manager->AddNewCameraModifier(ULPRoomBoundsCameraModifier::StaticClass())); if (!Modifier) { return; } } AActor* Room = Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom)); AActor* SceneCamera = IsValid(Room) ? Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomSceneCamera)) : nullptr; const AActor* View = PC->GetViewTarget(); const bool bListed = Label == LPCameraModeLocal::LabelScene || Label == LPCameraModeLocal::LabelC3Couple || Label == LPCameraModeLocal::LabelC3RoomOld; const bool bCandidate = !bMenu && bListed && IsValid(SceneCamera) && View == SceneCamera; FVector Pivot = FVector::ZeroVector; const bool bPivot = ScenePivot(GI, Pivot); // Piece visible : zone interieure (murs Wall* de Room), sol et plafond, avec les bornes partagees du recadrage. bool bRoomBounds = false; FBox Allowed(ForceInit); LPCameraModeLocal::FRoomInterior Interior; if (bCandidate && !Room->IsHidden() && LPCameraModeLocal::ComputeRoomInterior(Room, Interior)) { const FBox2D& Area = Interior.Area; const double Margin = LPCameraModeLocal::SceneWallMargin; const double ZMin = Interior.FloorTop + LPCameraModeLocal::SceneFloorMargin; FVector Min(Area.Min.X + Margin, Area.Min.Y + Margin, ZMin); FVector Max(Area.Max.X - Margin, Area.Max.Y - Margin, FMath::Max(ZMin, Interior.ViewCeiling - LPCameraModeLocal::SceneCeilingMargin)); if (Min.X > Max.X) { Min.X = Max.X = Area.GetCenter().X; } if (Min.Y > Max.Y) { Min.Y = Max.Y = Area.GetCenter().Y; } Allowed = FBox(Min, Max); bRoomBounds = true; } // Mur du studio (GI.Wall, panneau escamotable) visible : la vue reste du cote du pivot, devant le panneau (axe mince). bool bWallBounds = false; int32 WallAxis = 0; double WallLimit = 0.0; double WallSign = 1.0; FBox2D WallSpan(ForceInit); const AActor* Wall = bCandidate ? Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIWall)) : nullptr; if (IsValid(Wall) && !Wall->IsHidden() && bPivot) { const FBox Box = Wall->GetComponentsBoundingBox(true); if (Box.IsValid) { const FVector Size = Box.GetSize(); const FVector Center = Box.GetCenter(); WallAxis = Size.X <= Size.Y ? 0 : 1; const int32 Other = 1 - WallAxis; WallSign = Pivot[WallAxis] >= Center[WallAxis] ? 1.0 : -1.0; WallLimit = Center[WallAxis] + WallSign * (0.5 * Size[WallAxis] + LPCameraModeLocal::SceneWallMargin); WallSpan = FBox2D(FVector2D(Box.Min[Other], Box.Min.Z), FVector2D(Box.Max[Other], Box.Max.Z)); bWallBounds = true; } } const bool bActive = bCandidate && (bRoomBounds || bWallBounds); Modifier->SetBounds(bActive ? SceneCamera : nullptr, bRoomBounds, Allowed, bWallBounds, WallAxis, WallLimit, WallSign, WallSpan); // Temoin « Borne de vue » (§ 12.5, 2) : une ligne a chaque changement d'etat ou de libelle. static bool bWasActive = false; static FString LastLabel; if (bActive != bWasActive || Label != LastLabel) { bWasActive = bActive; LastLabel = Label; UE_LOG(LogLifePlay, Log, TEXT("Borne de vue : %s (%s) ; piece=%d mur du studio=%d ; haut de vue %.0f%s"), bActive ? TEXT("active") : TEXT("inactive"), Label.IsEmpty() ? TEXT("sans libelle") : *Label, bRoomBounds ? 1 : 0, bWallBounds ? 1 : 0, bRoomBounds ? Allowed.Max.Z : 0.0, Interior.bOutdoorSky ? TEXT(" (dehors : sol + 1000, WallT masque)") : TEXT("")); } // Temoin « Camera hors piece » (§ 12.5, 3) : la vue a du etre corrigee ; au plus une ligne par seconde. FVector Before = FVector::ZeroVector; FVector After = FVector::ZeroVector; FString What; static double NextLog = 0.0; const double Now = FPlatformTime::Seconds(); if (Modifier->ConsumeCorrection(Before, After, What) && Now >= NextLog) { NextLog = Now + LPCameraModeLocal::ViewBoundsLogInterval; const bool bLeft = PC->PlayerInput && PC->PlayerInput->IsPressed(EKeys::LeftMouseButton); const bool bRight = PC->PlayerInput && PC->PlayerInput->IsPressed(EKeys::RightMouseButton); UE_LOG(LogLifePlay, Log, TEXT("Camera hors piece : %s ; position (%.0f, %.0f, %.0f) -> (%.0f, %.0f, %.0f) ; zone X [%.0f, %.0f] Y [%.0f, %.0f] Z [%.0f, %.0f] ; borne franchie : %s ; distance au pivot %.0f ; MoveCamera=%d ; boutons G=%d D=%d"), *Label, Before.X, Before.Y, Before.Z, After.X, After.Y, After.Z, Allowed.Min.X, Allowed.Max.X, Allowed.Min.Y, Allowed.Max.Y, Allowed.Min.Z, Allowed.Max.Z, *What, bPivot ? FVector::Dist(After, Pivot) : -1.0, LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCMoveCamera) ? 1 : 0, bLeft ? 1 : 0, bRight ? 1 : 0); } } // Retrait du modificateur de vue (Deinitialize). static void RemoveViewBounds(UGameInstance* GI) { UWorld* World = GI ? GI->GetWorld() : nullptr; APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; APlayerCameraManager* Manager = PC ? PC->PlayerCameraManager.Get() : nullptr; if (UCameraModifier* Modifier = Manager ? Manager->FindCameraModifierByClass(ULPRoomBoundsCameraModifier::StaticClass()) : nullptr) { Manager->RemoveCameraModifier(Modifier); } } // § 12.9 : en scene, PC envoie aussi LeftRight a la vue de dessus (Birdview, invisible pendant la scene). Sa transformation est // memorisee a l'entree en scene et remise a la sortie : la carte ne garde aucun effet des touches de la scene. static void KeepBirdviewOverScene(UGameInstance* GI) { static bool bSexWas = false; static TWeakObjectPtr SavedActor; static FTransform Saved; const bool bSexOn = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn); AActor* Birdview = GI ? Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIBirdview)) : nullptr; if (bSexOn && !bSexWas && IsValid(Birdview)) { SavedActor = Birdview; Saved = Birdview->GetActorTransform(); } else if (!bSexOn && bSexWas && SavedActor.IsValid()) { AActor* Saving = SavedActor.Get(); const FVector Offset = Saving->GetActorLocation() - Saved.GetLocation(); if (!Saving->GetActorTransform().Equals(Saved, 0.1)) { Saving->SetActorTransform(Saved); UE_LOG(LogLifePlay, Log, TEXT("Scene : Birdview remis a sa place (decalage %.0f, %.0f, %.0f)"), Offset.X, Offset.Y, Offset.Z); } SavedActor.Reset(); } bSexWas = bSexOn; } // Vrai si Location (XY) est sur une des places CharGen de GI.ActorTransforms (lecture par reflexion, aucune ecriture de GI). static bool OnStudioSpot(const UObject* GI, const FVector& Location) { const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(LPCameraModeLocal::GIActorTransforms)) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; if (!Inner || Inner->Struct != TBaseStructure::Get()) { return false; } FScriptArrayHelper Array(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 Index = 0; Index < Array.Num(); ++Index) { const FTransform* Spot = reinterpret_cast(Array.GetRawPtr(Index)); if (FVector::Dist2D(Spot->GetLocation(), Location) <= LPCameraModeLocal::StudioSpotTolerance) { return true; } } return false; } // CAM2-SPEC-02 § 5 (S1-C++) : pendant la creation (GI.CharGenOn), la vue des cameras du studio (GI.ChargenCameras) est decalee // de StudioOffset par le modificateur, et le personnage edite (GI.CharacterBeingEdited) du meme vecteur : meme taille a l'ecran, // camera plus pres de la piece. Le personnage est redecale chaque fois que le BP le repose sur une place CharGen (ou en change), // et remis a sa place d'origine a la fermeture. Aucune ecriture de GI. static void TickStudioFraming(UGameInstance* GI) { UWorld* World = GI ? GI->GetWorld() : nullptr; APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; APlayerCameraManager* Manager = PC ? PC->PlayerCameraManager.Get() : nullptr; ULPRoomBoundsCameraModifier* Modifier = Manager ? Cast(Manager->FindCameraModifierByClass(ULPRoomBoundsCameraModifier::StaticClass())) : nullptr; const bool bStudio = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GICharGenOn); AActor* View = PC ? PC->GetViewTarget() : nullptr; const bool bStudioView = bStudio && IsValid(View) && LPCameraModeLocal::ArrayContains(GI, LPCameraModeLocal::GIChargenCameras, View); if (Modifier) { Modifier->SetStudio(bStudioView ? View : nullptr, LPCameraModeLocal::StudioOffset); } static bool bStudioViewWas = false; if (bStudioView != bStudioViewWas) { bStudioViewWas = bStudioView; UE_LOG(LogLifePlay, Log, TEXT("Studio : decalage de la vue %s (%s, decalage %.0f, %.0f, %.0f)"), bStudioView ? TEXT("actif") : TEXT("inactif"), *GetNameSafe(View), LPCameraModeLocal::StudioOffset.X, LPCameraModeLocal::StudioOffset.Y, LPCameraModeLocal::StudioOffset.Z); } static TWeakObjectPtr Shifted; static FVector Base = FVector::ZeroVector; AActor* Edited = bStudio ? Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GICharacterBeingEdited)) : nullptr; // Fin de la creation, ou (CAM2-VAL-03 R3) personnage edite change ou invalide : l'ancien acteur decale est remis a sa place // s'il est encore a la place decalee (un BP qui l'a deplace entre-temps l'emporte). if (!Shifted.IsValid() || Shifted.Get() != Edited || !IsValid(Edited)) { if (AActor* Back = Shifted.Get()) { const FVector Location = Back->GetActorLocation(); if (FVector::Dist2D(Location, Base + LPCameraModeLocal::StudioOffset) <= LPCameraModeLocal::StudioSpotTolerance) { Back->SetActorLocation(FVector(Base.X, Base.Y, Location.Z)); UE_LOG(LogLifePlay, Log, TEXT("Studio : personnage %s remis a sa place (%.0f, %.0f)%s"), *GetNameSafe(Back), Base.X, Base.Y, bStudio ? TEXT(" (personnage edite change)") : TEXT("")); } } Shifted.Reset(); } if (IsValid(Edited)) { const FVector Location = Edited->GetActorLocation(); const bool bNewActor = Shifted.Get() != Edited; const bool bReposed = !bNewActor && FVector::Dist2D(Location, Base + LPCameraModeLocal::StudioOffset) > LPCameraModeLocal::StudioSpotTolerance && OnStudioSpot(GI, Location); if ((bNewActor && OnStudioSpot(GI, Location)) || bReposed) { Base = Location; Shifted = Edited; Edited->SetActorLocation(Location + LPCameraModeLocal::StudioOffset); UE_LOG(LogLifePlay, Log, TEXT("Studio : personnage %s decale de (%.0f, %.0f, %.0f) depuis la place (%.0f, %.0f, %.0f)%s"), *GetNameSafe(Edited), LPCameraModeLocal::StudioOffset.X, LPCameraModeLocal::StudioOffset.Y, LPCameraModeLocal::StudioOffset.Z, Location.X, Location.Y, Location.Z, bReposed ? TEXT(" (repose par le BP)") : TEXT("")); } } } 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(); ClearC1HiddenOnSexEntry(GetGameInstance()); 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); // CAM2-SPEC-01 rev. 3 § 12.2 : le tangage voulu va de C1PitchMin a C1PitchMax ; le temoin ne signale qu'au-dela (marge 5). const double CameraPitch = FRotator::NormalizeAxis(CameraRotation.Pitch); if (Distance <= LPCameraModeLocal::C1MaxEyeDistance && CameraPitch >= LPCameraModeLocal::C1PitchMin - 5.f && CameraPitch <= LPCameraModeLocal::C1PitchMax + 5.f && 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); // CAM2-SPEC-01 rev. 3 : libelle calcule a chaque image (le libelle affiche suit Tick, a 0,1 s), pour que la borne de vue // et le tangage C1 changent dans l'image meme ou la vue change. UGameInstance* FrameGI = GetGameInstance(); const FString FrameLabel = ComputeLabel(); TickViewBounds(FrameGI, FrameLabel, bMenuOnScreen); TickStudioFraming(FrameGI); ResetC1PitchOnExit(FrameGI, FrameLabel == LPCameraModeLocal::LabelC1); if (!bMenuOnScreen && FrameLabel == LPCameraModeLocal::LabelC1) { TickC1Pitch(FrameGI); } KeepBirdviewOverScene(FrameGI); 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); } // CAM2-SPEC-01 rev. 3 § 12.8 : bornes Z partagees avec la borne de vue et CameraHeight (memes valeurs qu'avant). const double ZMin = Interior.FloorTop + LPCameraModeLocal::SceneFloorMargin; Desired.Z = FMath::Clamp(Desired.Z, ZMin, FMath::Max(ZMin, Interior.ViewCeiling - LPCameraModeLocal::SceneCeilingMargin)); } // Comme un bras de camera : devant le premier obstacle entre la cible et la camera (couple et cameras ignores). // CAM2-SPEC-01 rev. 3 § 12.3 : F2-a retire (camRoom est la camera C2, pas un mur) ; canal ECC_Visibility d'origine. 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("")); } // CAM2-VAL-03 R2 : numero de la scene en cours, augmente a chaque debut de scene (TickSceneKeys y releve le Z d'entree du couple). static uint32 SexSceneEntryCount = 0; 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) { ++SexSceneEntryCount; 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)); // KEY2 (KEY2-SPEC-01 § 8c) : touche d'action, hauteur de la camera de scene, couple sur Y. TickSceneKeys(PC, View, Room, Couple, bGrabbing, Step); 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 « POV souris » (CAM2-DIAG-02 § 5 et § 7.1, lecture seule) : en POV (index de vue non nul), souris cumulee depuis la // ligne precedente (CursorX / CursorY), touches, MoveCamera, cible de vue, personne que le BP tourne (PC 316 / 204 : // SexPersons[index-1].FirstPersonSM), rotations monde et relative, ecart depuis la ligne precedente. Au plus une ligne par seconde. { static float PovSumX = 0.f; static float PovSumY = 0.f; static float PovWait = 0.f; static FRotator PovLastRotation = FRotator::ZeroRotator; static TWeakObjectPtr PovLastLook; const int32 PovIndex = LPCameraModeLocal::ReadInt(GI, LPCameraModeLocal::GISexCameraIndex); PovWait = FMath::Max(0.f, PovWait - DeltaTime); if (PovIndex > 0) { PovSumX += LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisCursorX); PovSumY += LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisCursorY); const bool bPovActivity = !FMath::IsNearlyZero(PovSumX) || !FMath::IsNearlyZero(PovSumY) || bKeys || bLeftWasDown || bRightWasDown; if (bPovActivity && PovWait <= 0.f) { PovWait = LPCameraModeLocal::PovWitnessInterval; // Couple = SexPersons valides, dans l'ordre de GI (ReadActors) ; le BP prend SexPersons[index - 1] (PC 309). const AActor* Turned = Couple.IsValidIndex(PovIndex - 1) ? Couple[PovIndex - 1] : nullptr; const USceneComponent* ViewLook = IsValid(View) ? Cast(LPCameraModeLocal::ReadObject(View, LPCameraModeLocal::PlayerFirstPersonSM)) : nullptr; const USceneComponent* TurnedLook = IsValid(Turned) ? Cast(LPCameraModeLocal::ReadObject(Turned, LPCameraModeLocal::PlayerFirstPersonSM)) : nullptr; const FRotator PovRotation = IsValid(ViewLook) ? ViewLook->GetComponentRotation() : FRotator::ZeroRotator; const FRotator PovRelative = IsValid(ViewLook) ? ViewLook->GetRelativeRotation() : FRotator::ZeroRotator; const FRotator TurnedWorld = IsValid(TurnedLook) ? TurnedLook->GetComponentRotation() : FRotator::ZeroRotator; const FRotator PovMoved = PovLastLook.Get() == ViewLook ? (PovRotation - PovLastRotation).GetNormalized() : FRotator::ZeroRotator; UE_LOG(LogLifePlay, Log, TEXT("POV souris : CursorX=%+.2f CursorY=%+.2f (cumules) ; touches avant=%+.0f cote=%+.0f ; MoveCamera=%d boutons G=%d D=%d ; index %d ; vue %s ; tournee par le BP %s (%s) ; FirstPersonSM de la vue monde (P %+.1f, Y %+.1f, R %+.1f) relatif (P %+.1f, Y %+.1f, R %+.1f) ; ecart depuis la ligne precedente (P %+.1f, Y %+.1f, R %+.1f) ; FirstPersonSM tourne par le BP monde (P %+.1f, Y %+.1f, R %+.1f)"), PovSumX, PovSumY, Forward, Right, LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCMoveCamera) ? 1 : 0, bLeftWasDown ? 1 : 0, bRightWasDown ? 1 : 0, PovIndex, *GetNameSafe(View), *GetNameSafe(Turned), Turned == View ? TEXT("meme acteur que la vue") : TEXT("AUTRE acteur que la vue"), PovRotation.Pitch, PovRotation.Yaw, PovRotation.Roll, PovRelative.Pitch, PovRelative.Yaw, PovRelative.Roll, PovMoved.Pitch, PovMoved.Yaw, PovMoved.Roll, TurnedWorld.Pitch, TurnedWorld.Yaw, TurnedWorld.Roll); PovLastRotation = PovRotation; PovLastLook = ViewLook; PovSumX = 0.f; PovSumY = 0.f; } } else { PovSumX = 0.f; PovSumY = 0.f; PovLastLook.Reset(); } } // 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::TickSceneKeys(APlayerController* PC, AActor* View, const UObject* Room, const TArray& Couple, bool bGrabbing, float Step) { // KEY2 (§ 8c, 1) : la touche d'action bascule le panneau d'action, par le meme evenement que TickPanelClick. if (LPCameraModeLocal::ActionJustPressed(PC, LPCameraModeLocal::ActionPanelAction)) { UFunction* PanelToggle = PC->FindFunction(LPCameraModeLocal::PCLeftMouseSexOn); if (PanelToggle && PanelToggle->NumParms == 0) { UE_LOG(LogLifePlay, Log, TEXT("Scene : panneau d'action bascule (ActionPanel, touche %s)"), *LPCameraModeLocal::MappedKeys(LPCameraModeLocal::ActionPanelAction, 0)); PC->ProcessEvent(PanelToggle, nullptr); } else { UE_LOG(LogLifePlay, Log, TEXT("Scene : panneau d'action non bascule, evenement LeftMouseSexOn absent du PC")); } } // KEY2 (§ 8c, 5) + CAM2-SPEC-01 rev. 3 § 12.8 : CameraHeight monte ou baisse SceneCamera, rien pendant une prise. Piece // visible : hauteur absolue entre le dessus du sol + SceneFloorMargin et le plafond - SceneCeilingMargin (memes bornes que le // recadrage et la borne de vue). Piece cachee : borne relative d'origine [CameraHeightMin, CameraHeightMax] depuis le recadrage. AActor* LiftCamera = Cast(LPCameraModeLocal::ReadObject(Room, LPCameraModeLocal::RoomSceneCamera)); const bool bLiftC3 = LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCSexC3); if (!IsValid(LiftCamera) || View != LiftCamera || LastSexView.Get() != LiftCamera || (bLiftC3 && !bLiftC3Was)) { // Hors vue Scene, ou recadrage a venir (entree en Scene, passage en C3) : la camera reprend sa hauteur par defaut. SceneCameraLift = 0.f; } else if (!bGrabbing) { const float HeightAxis = LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisCameraHeight); if (!FMath::IsNearlyZero(HeightAxis)) { const double Z = LiftCamera->GetActorLocation().Z; double NewZ = Z + static_cast(HeightAxis * LPCameraModeLocal::CameraHeightSpeed * Step); const AActor* LiftRoom = Cast(Room); LPCameraModeLocal::FRoomInterior Interior; if (LiftRoom && !LiftRoom->IsHidden() && LPCameraModeLocal::ComputeRoomInterior(LiftRoom, Interior)) { const double ZMin = Interior.FloorTop + LPCameraModeLocal::SceneFloorMargin; NewZ = FMath::Clamp(NewZ, ZMin, FMath::Max(ZMin, Interior.ViewCeiling - LPCameraModeLocal::SceneCeilingMargin)); } else { const double Lift = FMath::Clamp(static_cast(SceneCameraLift) + (NewZ - Z), static_cast(LPCameraModeLocal::CameraHeightMin), static_cast(LPCameraModeLocal::CameraHeightMax)); NewZ = Z + (Lift - static_cast(SceneCameraLift)); } if (!FMath::IsNearlyEqual(NewZ, Z)) { LiftCamera->AddActorWorldOffset(FVector(0.0, 0.0, NewZ - Z)); SceneCameraLift += static_cast(NewZ - Z); } } } bLiftC3Was = bLiftC3; // CAM2-SPEC-02 § 7 (decision de l'utilisateur) : en scene, V / X (axe CoupleMoveY) montent et baissent le couple (Z) ; fleches // haut / bas (DownUp) = Y, gauche / droite (LeftRight) = X, repere monde (bCoupleKeysCameraFrame). Piece visible ou cachee : // PC ne deplace plus le couple ni la vue de dessus en scene (coupures BP de IfThenElse_11.then et IfThenElse_12.then), tout // passe ici. 5 cm par image et par unite d'axe. Piece visible : pivot a CoupleWallMargin (80 cm) des murs et racines du maillage // entre le sol et le plafond, les bornes de TickCoupleBounds (pas de saccade). Piece cachee : pas de borne XY ; Z du pivot dans // [Z d'entree - 30, Z d'entree + 150]. const AActor* MoveRoom = Cast(Room); const bool bRoomVisible = MoveRoom && !MoveRoom->IsHidden(); const float AxisZ = LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisCoupleMoveY); const float AxisUp = LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisDownUp); const float AxisRight = LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisLeftRight); const bool bAnyAxis = !FMath::IsNearlyZero(AxisZ) || !FMath::IsNearlyZero(AxisUp) || !FMath::IsNearlyZero(AxisRight); // CAM2-VAL-03 R4 : piece visible sans interieur calcule = aucune borne connue, le deplacement est refuse. LPCameraModeLocal::FRoomInterior Interior; const bool bInterior = bAnyAxis && bRoomVisible && LPCameraModeLocal::ComputeRoomInterior(MoveRoom, Interior); const TCHAR* Blocked = bGrabbing ? TEXT("prise en cours") : Couple.Num() == 0 ? TEXT("couple vide") : !MoveRoom ? TEXT("piece inconnue") : (bAnyAxis && bRoomVisible && !bInterior) ? TEXT("interieur inconnu") : nullptr; // Z d'entree en scene (piece cachee) : releve quand le meneur du couple change. CAM2-VAL-03 R2 : remis a zero quand le couple se // vide et a chaque debut de scene (SexSceneEntryCount), meme si le meneur reste le meme acteur. Un menu ouvert pendant la scene // (pas de TickSceneKeys) ne le remet pas a zero. static TWeakObjectPtr EntryLeader; static double EntryZ = 0.0; static uint32 EntryScene = 0; if (Couple.Num() == 0 || EntryScene != SexSceneEntryCount) { EntryLeader.Reset(); EntryScene = SexSceneEntryCount; } FVector Pivot = FVector::ZeroVector; if (Couple.Num() > 0) { for (const AActor* Person : Couple) { Pivot += Person->GetActorLocation(); } Pivot /= Couple.Num(); if (EntryLeader.Get() != Couple[0]) { EntryLeader = Couple[0]; EntryZ = Pivot.Z; } } FVector MoveStep = FVector::ZeroVector; FString BoundHit; if (!Blocked && bAnyAxis) { FVector Forward(0.0, 1.0, 0.0); FVector Right(1.0, 0.0, 0.0); if (LPCameraModeLocal::bCoupleKeysCameraFrame && IsValid(View)) { const FVector ViewForward = View->GetActorForwardVector(); Forward = FVector(ViewForward.X, ViewForward.Y, 0.0).GetSafeNormal(); Right = FVector(-Forward.Y, Forward.X, 0.0); } const double Step5 = static_cast(LPCameraModeLocal::CoupleMoveYStep); MoveStep = Forward * (LPCameraModeLocal::CoupleKeyUpSign * AxisUp * Step5) + Right * (LPCameraModeLocal::CoupleKeyRightSign * AxisRight * Step5) + FVector(0.0, 0.0, AxisZ * Step5); if (bInterior) { const FBox2D& Area = Interior.Area; const double Margin = LPCameraModeLocal::CoupleWallMargin; FVector NewPivot = Pivot + MoveStep; NewPivot.X = Area.Max.X - Area.Min.X > 2.0 * Margin ? FMath::Clamp(NewPivot.X, Area.Min.X + Margin, Area.Max.X - Margin) : Pivot.X; NewPivot.Y = Area.Max.Y - Area.Min.Y > 2.0 * Margin ? FMath::Clamp(NewPivot.Y, Area.Min.Y + Margin, Area.Max.Y - Margin) : Pivot.Y; if (!FMath::IsNearlyEqual(NewPivot.X - Pivot.X, MoveStep.X, 0.01) || !FMath::IsNearlyEqual(NewPivot.Y - Pivot.Y, MoveStep.Y, 0.01)) { BoundHit += TEXT("mur "); } // Z : racines du maillage entre le dessus du sol (tolerance) et le plafond moins la marge de hauteur (TickCoupleBounds). 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); } double DeltaZ = MoveStep.Z; const double FloorLimit = Interior.FloorTop - LPCameraModeLocal::CoupleFloorTolerance; const double CeilingLimit = Interior.Ceiling - LPCameraModeLocal::CoupleHeadroom; if (DeltaZ < 0.0 && LowestRoot + DeltaZ < FloorLimit) { DeltaZ = FMath::Min(0.0, FloorLimit - LowestRoot); BoundHit += TEXT("sol "); } else if (DeltaZ > 0.0 && HighestRoot + DeltaZ > CeilingLimit) { DeltaZ = FMath::Max(0.0, CeilingLimit - HighestRoot); BoundHit += TEXT("plafond "); } MoveStep = FVector(NewPivot.X - Pivot.X, NewPivot.Y - Pivot.Y, DeltaZ); } else { // Piece cachee (piece visible sans interieur : refusee plus haut). const double NewZ = FMath::Clamp(Pivot.Z + MoveStep.Z, EntryZ - LPCameraModeLocal::CoupleOutdoorZBelow, EntryZ + LPCameraModeLocal::CoupleOutdoorZAbove); if (!FMath::IsNearlyEqual(NewZ - Pivot.Z, MoveStep.Z, 0.01)) { BoundHit += TEXT("hauteur dehors "); } MoveStep.Z = NewZ - Pivot.Z; } if (!MoveStep.IsNearlyZero()) { for (AActor* Person : Couple) { Person->SetActorLocation(Person->GetActorLocation() + MoveStep); } } } // Temoin (§ 7.2) : une ligne au relachement de chaque axe (deplacement cumule de l'appui, bornes atteintes), ou a l'appui ignore. struct FCoupleMoveWitness { float Sign = 0.f; bool bIgnored = false; FString Bounds; FVector Moved = FVector::ZeroVector; }; static FCoupleMoveWitness WitnessZ; static FCoupleMoveWitness WitnessUp; static FCoupleMoveWitness WitnessRight; const FString Hit = BoundHit.TrimEnd(); auto Witness = [&](FCoupleMoveWitness& State, const TCHAR* AxisName, float Value) { const float Sign = FMath::IsNearlyZero(Value) ? 0.f : FMath::Sign(Value); if (Sign != 0.f && State.Sign == 0.f) { State = FCoupleMoveWitness(); State.Sign = Sign; State.bIgnored = Blocked != nullptr; if (Blocked) { UE_LOG(LogLifePlay, Log, TEXT("Scene : %s=%+.0f ignore (%s)"), AxisName, Sign, Blocked); } } if (Sign != 0.f && !State.bIgnored) { State.Moved += MoveStep; if (!Hit.IsEmpty() && !State.Bounds.Contains(Hit)) { State.Bounds += (State.Bounds.IsEmpty() ? TEXT("") : TEXT(" ")) + Hit; } } if (Sign == 0.f && State.Sign != 0.f) { if (!State.bIgnored) { UE_LOG(LogLifePlay, Log, TEXT("Scene : %s=%+.0f, Room cachee=%d, couple deplace de (%+.0f, %+.0f, %+.0f) [borne : %s]"), AxisName, State.Sign, bRoomVisible ? 0 : 1, State.Moved.X, State.Moved.Y, State.Moved.Z, State.Bounds.IsEmpty() ? TEXT("non") : *State.Bounds); } State = FCoupleMoveWitness(); } }; Witness(WitnessZ, TEXT("CoupleMoveY (Z)"), AxisZ); Witness(WitnessUp, TEXT("DownUp (Y)"), AxisUp); Witness(WitnessRight, TEXT("LeftRight (X)"), AxisRight); } 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)); // KEY2 (KEY2-SPEC-01 § 8b, decision 13) : « Arrows only » (CoupleDrag=Keys), le clic gauche ne prend plus le // couple et reste au jeu ; le clic droit garde la rotation. const ULPKeyboardSubsystem* DragKeys = GI->GetSubsystem(); if (DragKeys && DragKeys->IsCoupleDragByKeys() && bLeftPressed) { return false; } 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" " %s / %s: raise / lower the camera\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; off in Arrows only mode)\n" " Right-drag the couple: turn it\n" " %s / %s: couple up / down (raise / lower) Mouse wheel: raise / lower the couple\n" " %s: move the couple forward / back (Y) %s: move the couple right / left (X)\n" "ACTIONS\n" " %s, or Alt / middle click (short press): show / hide the action panel"), *MappedKeys(LPCameraModeLocal::SwitchCameraAction, 0), *MappedKeys(LPCameraModeLocal::AxisMoveForward, 1), *MappedKeys(LPCameraModeLocal::AxisMoveForward, -1), *MappedKeys(LPCameraModeLocal::AxisMoveRight, -1), *MappedKeys(LPCameraModeLocal::AxisMoveRight, 1), *MappedKeys(LPCameraModeLocal::AxisCameraHeight, 1), *MappedKeys(LPCameraModeLocal::AxisCameraHeight, -1), *MappedKeys(LPCameraModeLocal::AxisCoupleMoveY, 1), *MappedKeys(LPCameraModeLocal::AxisCoupleMoveY, -1), *MappedKeys(LPCameraModeLocal::AxisDownUp, 0), *MappedKeys(LPCameraModeLocal::AxisLeftRight, 0), *MappedKeys(LPCameraModeLocal::ActionPanelAction, 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(); }