2026-10-19 20:57:42 +00:00
// Lot CAM-B2c - voir LPCameraModeSubsystem.h
# include "LPCameraModeSubsystem.h"
# include "Blueprint/UserWidget.h"
# include "Blueprint/WidgetTree.h"
2026-10-20 15:35:10 +00:00
# include "Camera/PlayerCameraManager.h"
# include "LPRoomBoundsCameraModifier.h"
2026-10-19 20:57:42 +00:00
# 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
2026-10-20 15:35:10 +00:00
// 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 ;
2026-10-19 20:57:42 +00:00
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)
2026-10-20 15:35:10 +00:00
// 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 ;
2026-10-19 20:57:42 +00:00
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)
2026-10-20 15:35:10 +00:00
// 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 ;
2026-10-19 20:57:42 +00:00
// 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 ;
2026-10-20 15:35:10 +00:00
// 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
2026-10-19 20:57:42 +00:00
// 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 < FObjectPropertyBase > ( Container - > GetClass ( ) - > FindPropertyByName ( Name ) ) : nullptr ;
return Prop ? Prop - > GetObjectPropertyValue_InContainer ( Container ) : nullptr ;
}
bool ReadBool ( const UObject * Container , FName Name )
{
const FBoolProperty * Prop = Container ? CastField < FBoolProperty > ( 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 < FBoolProperty > ( 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 < FStructProperty > ( Container - > GetClass ( ) - > FindPropertyByName ( Name ) ) : nullptr ;
if ( ! Prop | | ! World | | Prop - > Struct ! = FTimerHandle : : StaticStruct ( ) )
{
return false ;
}
return World - > GetTimerManager ( ) . IsTimerActive ( * Prop - > ContainerPtrToValuePtr < FTimerHandle > ( Container ) ) ;
}
int32 ReadInt ( const UObject * Container , FName Name )
{
const FIntProperty * Prop = Container ? CastField < FIntProperty > ( Container - > GetClass ( ) - > FindPropertyByName ( Name ) ) : nullptr ;
return Prop ? Prop - > GetPropertyValue_InContainer ( Container ) : - 1 ;
}
// Acteurs valides du tableau d'objets Name de Container.
TArray < AActor * > ReadActors ( const UObject * Container , FName Name )
{
TArray < AActor * > Out ;
const FArrayProperty * Prop = Container ? CastField < FArrayProperty > ( Container - > GetClass ( ) - > FindPropertyByName ( Name ) ) : nullptr ;
const FObjectPropertyBase * Inner = Prop ? CastField < FObjectPropertyBase > ( Prop - > Inner ) : nullptr ;
if ( ! Inner )
{
return Out ;
}
FScriptArrayHelper Array ( Prop , Prop - > ContainerPtrToValuePtr < void > ( Container ) ) ;
for ( int32 Index = 0 ; Index < Array . Num ( ) ; + + Index )
{
AActor * Actor = Cast < AActor > ( 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 ) ;
}
2026-10-20 15:35:10 +00:00
// 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 ;
}
2026-10-19 20:57:42 +00:00
// 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 < FString > 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 < FStructProperty > ( Container - > GetClass ( ) - > FindPropertyByName ( Name ) ) : nullptr ;
if ( ! Prop | | Prop - > Struct ! = TBaseStructure < FVector > : : Get ( ) )
{
return false ;
}
Out = * Prop - > ContainerPtrToValuePtr < FVector > ( 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 < FArrayProperty > ( Container - > GetClass ( ) - > FindPropertyByName ( Name ) ) : nullptr ;
const FObjectPropertyBase * Inner = Prop ? CastField < FObjectPropertyBase > ( Prop - > Inner ) : nullptr ;
if ( ! Inner )
{
return false ;
}
FScriptArrayHelper Array ( Prop , Prop - > ContainerPtrToValuePtr < void > ( 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 < ULPKeyboardSubsystem > ( ) )
{
if ( Keys - > IsMenuOpen ( ) )
{
return true ;
}
}
for ( TObjectIterator < UUserWidget > 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 < SWidget > 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 ;
2026-10-20 15:35:10 +00:00
// 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 ;
2026-10-19 20:57:42 +00:00
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 < UPrimitiveComponent > ( 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 < UPrimitiveComponent * > 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 < FArrayProperty > ( Room - > GetClass ( ) - > FindPropertyByName ( RoomAllComponents ) ) ;
const FObjectPropertyBase * Inner = Prop ? CastField < FObjectPropertyBase > ( Prop - > Inner ) : nullptr ;
if ( Inner )
{
FScriptArrayHelper Array ( Prop , Prop - > ContainerPtrToValuePtr < void > ( Room ) ) ;
for ( int32 Index = 0 ; Index < FMath : : Min ( Array . Num ( ) , RoomStructureCount ) ; + + Index )
{
if ( UseStructure ( Cast < UPrimitiveComponent > ( 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) " ) ;
}
2026-10-20 15:35:10 +00:00
// 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 < USceneComponent > ( ReadObject ( Room , RoomCeilingWallT ) ) ;
if ( IsValid ( CeilingWallT ) & & CeilingWallT - > bHiddenInGame )
{
Out . bOutdoorSky = true ;
Out . ViewCeiling = Out . FloorTop + OutdoorViewCeilingHeight ;
}
2026-10-19 20:57:42 +00:00
return true ;
}
// Z de la racine du maillage la plus basse du couple (acteur a defaut de maillage).
double CoupleLowestRoot ( const TArray < AActor * > & Couple )
{
double Lowest = TNumericLimits < double > : : Max ( ) ;
for ( const AActor * Person : Couple )
{
const ACharacter * Character = Cast < ACharacter > ( 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 < UStaticMeshComponent > ( 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 < AActor * > & 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 < FOverlapResult > 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 < SWidget > FocusedEditable ( )
{
if ( ! FSlateApplication : : IsInitialized ( ) )
{
return nullptr ;
}
TSharedPtr < SWidget > 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 < UUserWidget > 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 ) ;
}
2026-10-20 15:35:10 +00:00
static void RemoveViewBounds ( UGameInstance * GI ) ;
2026-10-19 20:57:42 +00:00
void ULPCameraModeSubsystem : : Deinitialize ( )
{
FTSTicker : : GetCoreTicker ( ) . RemoveTicker ( SwitchKeyHandle ) ;
FTSTicker : : GetCoreTicker ( ) . RemoveTicker ( TickHandle ) ;
2026-10-20 15:35:10 +00:00
RemoveViewBounds ( GetGameInstance ( ) ) ;
2026-10-19 20:57:42 +00:00
RemoveWidget ( ) ;
RemoveHelpBox ( ) ;
Super : : Deinitialize ( ) ;
}
2026-10-20 15:35:10 +00:00
// 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 < double > ( LPCameraModeLocal : : C1PitchMin ) , static_cast < double > ( 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 < ACharacter > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIPlayerCharacter ) ) ;
const USceneComponent * Camera = IsValid ( Player ) ? Cast < USceneComponent > ( 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 < AActor * > 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 < AActor > ( 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 < ULPRoomBoundsCameraModifier > ( Manager - > FindCameraModifierByClass ( ULPRoomBoundsCameraModifier : : StaticClass ( ) ) ) ;
if ( ! Modifier )
{
Modifier = Cast < ULPRoomBoundsCameraModifier > ( Manager - > AddNewCameraModifier ( ULPRoomBoundsCameraModifier : : StaticClass ( ) ) ) ;
if ( ! Modifier )
{
return ;
}
}
AActor * Room = Cast < AActor > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIRoom ) ) ;
AActor * SceneCamera = IsValid ( Room ) ? Cast < AActor > ( 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 < AActor > ( 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 < AActor > SavedActor ;
static FTransform Saved ;
const bool bSexOn = GI & & LPCameraModeLocal : : ReadBool ( GI , LPCameraModeLocal : : GISexOn ) ;
AActor * Birdview = GI ? Cast < AActor > ( 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 < FArrayProperty > ( GI - > GetClass ( ) - > FindPropertyByName ( LPCameraModeLocal : : GIActorTransforms ) ) : nullptr ;
const FStructProperty * Inner = Prop ? CastField < FStructProperty > ( Prop - > Inner ) : nullptr ;
if ( ! Inner | | Inner - > Struct ! = TBaseStructure < FTransform > : : Get ( ) )
{
return false ;
}
FScriptArrayHelper Array ( Prop , Prop - > ContainerPtrToValuePtr < void > ( GI ) ) ;
for ( int32 Index = 0 ; Index < Array . Num ( ) ; + + Index )
{
const FTransform * Spot = reinterpret_cast < const FTransform * > ( 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 < ULPRoomBoundsCameraModifier > ( 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 < AActor > Shifted ;
static FVector Base = FVector : : ZeroVector ;
AActor * Edited = bStudio ? Cast < AActor > ( 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 ( " " ) ) ;
}
}
}
2026-10-19 20:57:42 +00:00
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 ( ) ;
2026-10-20 15:35:10 +00:00
ClearC1HiddenOnSexEntry ( GetGameInstance ( ) ) ;
2026-10-19 20:57:42 +00:00
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 < ACharacter > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIPlayerCharacter ) ) ;
const USkeletalMeshComponent * Mesh = IsValid ( Player ) ? Player - > GetMesh ( ) : nullptr ;
const USceneComponent * Camera = IsValid ( Player ) ? Cast < USceneComponent > ( 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 ) ;
2026-10-20 15:35:10 +00:00
// 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 )
2026-10-19 20:57:42 +00:00
{
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 ) ;
2026-10-20 15:35:10 +00:00
// 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 ) ;
2026-10-19 20:57:42 +00:00
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 < AActor > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIRoom ) ) ;
const TArray < AActor * > 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 < UPrimitiveComponent > ( 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 < double > : : Max ( ) ;
double HighestRoot = TNumericLimits < double > : : Lowest ( ) ;
for ( const AActor * Person : Couple )
{
const ACharacter * Character = Cast < ACharacter > ( 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 < AActor * > & 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 < AActor > ( 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 < AActor * > & 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 < double > : : Max ( ) ;
for ( const AActor * Person : Couple )
{
const ACharacter * Character = Cast < ACharacter > ( 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 ) ;
}
2026-10-20 15:35:10 +00:00
// 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 ) ) ;
2026-10-19 20:57:42 +00:00
}
// Comme un bras de camera : devant le premier obstacle entre la cible et la camera (couple et cameras ignores).
2026-10-20 15:35:10 +00:00
// CAM2-SPEC-01 rev. 3 § 12.3 : F2-a retire (camRoom est la camera C2, pas un mur) ; canal ECC_Visibility d'origine.
2026-10-19 20:57:42 +00:00
FCollisionQueryParams Query ( SCENE_QUERY_STAT ( LPRecenter ) , true ) ;
Query . AddIgnoredActors ( Couple ) ;
Query . AddIgnoredActor ( SceneCamera ) ;
if ( AActor * Wall = Cast < AActor > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIWall ) ) )
{
Query . AddIgnoredActor ( Wall ) ;
}
if ( AActor * CamRoom = Cast < AActor > ( 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 < double > ( 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 ( " " ) ) ;
}
2026-10-20 15:35:10 +00:00
// 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 ;
2026-10-19 20:57:42 +00:00
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 )
{
2026-10-20 15:35:10 +00:00
+ + SexSceneEntryCount ;
2026-10-19 20:57:42 +00:00
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 < SWidget > 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 < SEditableText > ( 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 < AActor * > 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 < AActor > ( LPCameraModeLocal : : ReadObject ( Room , LPCameraModeLocal : : RoomSceneCamera ) ) ;
2026-10-20 15:35:10 +00:00
// KEY2 (KEY2-SPEC-01 § 8c) : touche d'action, hauteur de la camera de scene, couple sur Y.
TickSceneKeys ( PC , View , Room , Couple , bGrabbing , Step ) ;
2026-10-19 20:57:42 +00:00
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 < double > ( LPCameraModeLocal : : SexMinDistance ) , static_cast < double > ( 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 < USceneComponent > ( 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 < double > ( LPCameraModeLocal : : SexMaxPitch ) , static_cast < double > ( LPCameraModeLocal : : SexMaxPitch ) ) ;
Look - > SetWorldRotation ( Rotation ) ;
}
}
2026-10-20 15:35:10 +00:00
// 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 < const USceneComponent > 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 < USceneComponent > ( LPCameraModeLocal : : ReadObject ( View , LPCameraModeLocal : : PlayerFirstPersonSM ) ) : nullptr ;
const USceneComponent * TurnedLook = IsValid ( Turned ) ? Cast < USceneComponent > ( 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 ( ) ;
}
}
2026-10-19 20:57:42 +00:00
// 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 ;
}
2026-10-20 15:35:10 +00:00
void ULPCameraModeSubsystem : : TickSceneKeys ( APlayerController * PC , AActor * View , const UObject * Room , const TArray < AActor * > & 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 < AActor > ( 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 < double > ( HeightAxis * LPCameraModeLocal : : CameraHeightSpeed * Step ) ;
const AActor * LiftRoom = Cast < AActor > ( 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 < double > ( SceneCameraLift ) + ( NewZ - Z ) ,
static_cast < double > ( LPCameraModeLocal : : CameraHeightMin ) , static_cast < double > ( LPCameraModeLocal : : CameraHeightMax ) ) ;
NewZ = Z + ( Lift - static_cast < double > ( SceneCameraLift ) ) ;
}
if ( ! FMath : : IsNearlyEqual ( NewZ , Z ) )
{
LiftCamera - > AddActorWorldOffset ( FVector ( 0.0 , 0.0 , NewZ - Z ) ) ;
SceneCameraLift + = static_cast < float > ( 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 < AActor > ( 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 < AActor > 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 < double > ( 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 < double > : : Max ( ) ;
double HighestRoot = TNumericLimits < double > : : Lowest ( ) ;
for ( const AActor * Person : Couple )
{
const ACharacter * Character = Cast < ACharacter > ( 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 ) ;
}
2026-10-19 20:57:42 +00:00
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 < int32 > ( 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 < uint8 > ( ViewportClient - > GetMouseCaptureMode ( ) ) ;
SavedLockMode = static_cast < uint8 > ( 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 < int32 > ( SavedCaptureMode ) , static_cast < int32 > ( 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 < EMouseCaptureMode > ( 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 < EMouseLockMode > ( SavedLockMode ) ) ;
ViewportClient - > SetHideCursorDuringCapture ( bSavedHideCursor ) ;
InputText = FString : : Printf ( TEXT ( " rendue (capture %d verrou %d curseur cache %d) " ) ,
static_cast < int32 > ( SavedCaptureMode ) , static_cast < int32 > ( 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 < AActor * > & 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 < AActor > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIWall ) ) )
{
Query . AddIgnoredActor ( Wall ) ;
}
if ( AActor * CamRoom = Cast < AActor > ( 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 < float > ( Screen . X ) , static_cast < float > ( 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 < UPrimitiveComponent > ( LPCameraModeLocal : : ReadObject ( Room , LPCameraModeLocal : : RoomFloor ) ) ;
const AActor * RoomActor = Cast < AActor > ( 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 ) ) ;
2026-10-20 15:35:10 +00:00
// 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 < ULPKeyboardSubsystem > ( ) ;
if ( DragKeys & & DragKeys - > IsCoupleDragByKeys ( ) & & bLeftPressed )
{
return false ;
}
2026-10-19 20:57:42 +00:00
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 < ACharacter > ( 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 ;
2026-10-21 07:36:04 +00:00
FVector Aim = FVector : : ZeroVector ;
2026-10-19 20:57:42 +00:00
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 < ULPShowCoupleSubsystem > ( ) ;
// 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 ;
2026-10-21 07:36:04 +00:00
FVector Aim = FVector : : ZeroVector ;
2026-10-19 20:57:42 +00:00
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 < ACharacter > ( 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 < float > ( GrabCursor . X - GrabLastMouseX ) * LPCameraModeLocal : : CoupleTurnPerPixel ;
GrabLastMouseX = static_cast < float > ( 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 < AActor > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIRoom ) ) ;
ACharacter * Player = Cast < ACharacter > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIPlayerCharacter ) ) ;
AActor * Cam = IsValid ( Room ) ? Cast < AActor > ( 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 < AActor > ( 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 < UStaticMeshComponent > ( 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 < AActor > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIRoom ) ) ;
ACharacter * Player = Cast < ACharacter > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GIPlayerCharacter ) ) ;
AActor * Cam = IsValid ( Room ) ? Cast < AActor > ( 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 < AActor > ( 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 < APlayerController > ( 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 < STextBlock > NewText ;
TSharedRef < SWidget > 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 "
2026-10-20 15:35:10 +00:00
" %s / %s: raise / lower the camera \n "
2026-10-19 20:57:42 +00:00
" Hold a mouse button away from the couple and move the mouse: rotate the camera \n "
" COUPLE \n "
2026-10-20 15:35:10 +00:00
" Left-drag the couple: move it (floor or bed only; off in Arrows only mode) \n "
2026-10-19 20:57:42 +00:00
" Right-drag the couple: turn it \n "
2026-10-20 15:35:10 +00:00
" %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 "
2026-10-19 20:57:42 +00:00
" ACTIONS \n "
2026-10-20 15:35:10 +00:00
" %s, or Alt / middle click (short press): show / hide the action panel " ) ,
2026-10-19 20:57:42 +00:00
* MappedKeys ( LPCameraModeLocal : : SwitchCameraAction , 0 ) ,
* MappedKeys ( LPCameraModeLocal : : AxisMoveForward , 1 ) , * MappedKeys ( LPCameraModeLocal : : AxisMoveForward , - 1 ) ,
* MappedKeys ( LPCameraModeLocal : : AxisMoveRight , - 1 ) , * MappedKeys ( LPCameraModeLocal : : AxisMoveRight , 1 ) ,
2026-10-20 15:35:10 +00:00
* 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 ) ) ;
2026-10-19 20:57:42 +00:00
}
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 < UUserWidget > ( LPCameraModeLocal : : ReadObject ( GI , LPCameraModeLocal : : GISexInterface ) ) : nullptr ;
if ( UTextBlock * Lines = SexUI ? Cast < UTextBlock > ( 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 < UUserWidget > ( 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 < UButton > ( 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 < UCanvasPanelSlot > ( 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 > ( UButton : : StaticClass ( ) , LPCameraModeLocal : : HelpButtonName ) ;
IconButton - > SetBackgroundColor ( FLinearColor : : Transparent ) ;
UTexture2D * IconTexture = LoadObject < UTexture2D > ( nullptr , LPCameraModeLocal : : HelpIconPath ) ;
if ( IconTexture )
{
UImage * IconImage = ActionCursor - > WidgetTree - > ConstructWidget < UImage > ( 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 > ( 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 < UHorizontalBoxSlot > ( IconSlot ) )
{
RowSlot - > SetVerticalAlignment ( VAlign_Center ) ;
RowSlot - > SetPadding ( FMargin ( 4.f , 0.f , 0.f , 0.f ) ) ;
}
else if ( UCanvasPanelSlot * CanvasSlot = Cast < UCanvasPanelSlot > ( 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 < STextBlock > NewHelpText ;
TSharedRef < SWidget > 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 ( ) ;
}