2960 lines
134 KiB
C++
2960 lines
134 KiB
C++
// Lot CAM-B2c - voir LPCameraModeSubsystem.h
|
|
|
|
#include "LPCameraModeSubsystem.h"
|
|
|
|
#include "Blueprint/UserWidget.h"
|
|
#include "Blueprint/WidgetTree.h"
|
|
#include "Camera/PlayerCameraManager.h"
|
|
#include "LPRoomBoundsCameraModifier.h"
|
|
#include "Components/Button.h"
|
|
#include "Components/CanvasPanelSlot.h"
|
|
#include "Components/HorizontalBoxSlot.h"
|
|
#include "Components/Image.h"
|
|
#include "Components/PanelWidget.h"
|
|
#include "Components/TextBlock.h"
|
|
#include "Engine/Texture2D.h"
|
|
#include "Widgets/Layout/SBorder.h"
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/GameViewportClient.h"
|
|
#include "Engine/World.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
#include "Components/StaticMeshComponent.h"
|
|
#include "Engine/OverlapResult.h"
|
|
#include "Engine/StaticMesh.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "GameFramework/Actor.h"
|
|
#include "GameFramework/Character.h"
|
|
#include "GameFramework/InputSettings.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "GameFramework/PlayerInput.h"
|
|
#include "LPKeyboardSubsystem.h"
|
|
#include "LPShowCoupleSubsystem.h"
|
|
#include "LifePlay.h"
|
|
#include "Styling/CoreStyle.h"
|
|
#include "TimerManager.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "UObject/UnrealType.h"
|
|
#include "Widgets/Input/SEditableText.h"
|
|
#include "Widgets/SOverlay.h"
|
|
#include "Widgets/Text/STextBlock.h"
|
|
|
|
// Espace de noms nomme, pas anonyme : en compilation unitaire, ce fichier peut partager une unite avec
|
|
// LPDirectControlSubsystem.cpp et LPOutdoorRoomSubsystem.cpp, dont l'espace anonyme a des noms voisins
|
|
// (MenuClasses, MenuOnScreen, GetObjectProperty) : risque C2374 (lp-build). Appels toujours qualifies.
|
|
namespace LPCameraModeLocal
|
|
{
|
|
const float TickInterval = 0.1f;
|
|
const int32 ViewportZOrder = 50;
|
|
|
|
// Variables de GI et de Room (noms exacts du paragraphe 2 de la SPEC).
|
|
const FName GIBirdview(TEXT("Birdview"));
|
|
const FName GIChargenCameras(TEXT("ChargenCameras"));
|
|
const FName GIPlayerCharacter(TEXT("PlayerCharacter"));
|
|
const FName GIPC(TEXT("PC"));
|
|
const FName GIRoom(TEXT("Room"));
|
|
const FName GISexOn(TEXT("SexOn"));
|
|
// POVOn est une variable du PC (ecrite par SwitchCamera), pas de GI (CAM-COMMUN2-DIAG, KO 1).
|
|
const FName PCPOVOn(TEXT("POVOn"));
|
|
const FName GIBuilderOn(TEXT("BuilderOn"));
|
|
// Temoin touche C (CAM-B2j) : gardes du PC (BuilderOn, SexOn, SpaceOn) et etat C1 de Room.
|
|
const FName GISpaceOn(TEXT("SpaceOn"));
|
|
const FName RoomC1Hidden(TEXT("C1Hidden"));
|
|
const FName SwitchCameraAction(TEXT("SwitchCamera"));
|
|
const FName RoomCamC1On(TEXT("CamC1On"));
|
|
const FName RoomCamC3On(TEXT("CamC3On"));
|
|
const FName RoomCamRoom(TEXT("CamRoom"));
|
|
const FName RoomSceneCamera(TEXT("SceneCamera"));
|
|
const FName RoomC2BoxMin(TEXT("C2BoxMin"));
|
|
const FName RoomC2BoxMax(TEXT("C2BoxMax"));
|
|
const FName RoomC2Target(TEXT("C2Target"));
|
|
// Panneau de fond cache (StaticMeshActor, BlockAll) qui traverse les pieces : ignore par les sondes depuis B2g.
|
|
const FName GIWall(TEXT("Wall"));
|
|
|
|
// Temoin C2 (CAM-B2f) : memes depart (douille head), rayon et acteurs ignores que la sonde de Room.
|
|
const TCHAR* const LabelC2 = TEXT("Camera: C2 Follow");
|
|
const FName HeadSocket(TEXT("head"));
|
|
const float WitnessNearDistance = 100.f;
|
|
const float WitnessProbeLength = 250.f;
|
|
const float WitnessProbeRadius = 20.f;
|
|
const float WitnessInterval = 1.f;
|
|
// Temoin d'occlusion (CAM-Tocc) : trace toutes les 0,5 s, au plus une ligne par seconde.
|
|
const float OcclusionInterval = 0.5f;
|
|
const float OcclusionLogInterval = 1.f;
|
|
|
|
// Sortie de CharGen (CAM-B2r) : ShowRender (GI) remet SpaceOn a faux et CloseCharGen ne le remet pas en cours
|
|
// de partie (CAM-COMMUN6-DIAG § 10) ; MainMenuOn peut rester vrai si CharGen a ete ouvert du menu principal.
|
|
const FName GICharGenOn(TEXT("CharGenOn"));
|
|
const FName GIMainMenuOn(TEXT("MainMenuOn"));
|
|
const FName GIJournalOn(TEXT("JournalOn"));
|
|
const FName GIContactsOn(TEXT("ContactsOn"));
|
|
const FName PCMoveCamera(TEXT("MoveCamera"));
|
|
const TCHAR* const MainMenuClass = TEXT("MainMenu_C");
|
|
const float CharGenRepairDelay = 10.f;
|
|
const TCHAR* const RoomCameraLabels[] = {
|
|
TEXT("Camera: C1 First person"), TEXT("Camera: C2 Follow"), TEXT("Camera: C3 Ceiling"), TEXT("Camera: C3 Room (old)")
|
|
};
|
|
|
|
// Temoin C1 (CAM-B2r) : ecart de FirstPersonSM aux yeux et a l'horizontale, toutes les 0,5 s, au plus une ligne par seconde.
|
|
const TCHAR* const LabelC1 = TEXT("Camera: C1 First person");
|
|
const FName PlayerFirstPersonSM(TEXT("FirstPersonSM"));
|
|
const FName PlayerOccupyingFnt(TEXT("OccupyingFnt"));
|
|
const FName RoomCamTimer(TEXT("RoomCamTimer"));
|
|
const FName LeftEyeSocket(TEXT("lEye"));
|
|
const FName RightEyeSocket(TEXT("rEye"));
|
|
const float C1Interval = 0.5f;
|
|
const float C1LogInterval = 1.f;
|
|
const float C1MaxEyeDistance = 8.f;
|
|
const float C1MaxPitch = 25.f;
|
|
const float C1MaxYawGap = 30.f;
|
|
|
|
// Camera de scene au clavier (CAM-B2p, P3) : pendant le sexe, ZQSD pilote la camera en vue (la marche de STG
|
|
// est coupee pendant le sexe). Mode Scene : orbite (Q, D) et avance ou recul (Z, S) autour du couple ;
|
|
// POV : lacet (Q, D) et tangage (Z, S) de FirstPersonSM de la personne en vue. Aucun mappage n'est change.
|
|
const FName AxisMoveForward(TEXT("MoveForward"));
|
|
const FName AxisMoveRight(TEXT("MoveRight"));
|
|
const FName AxisLeftRight(TEXT("LeftRight"));
|
|
const FName AxisDownUp(TEXT("DownUp"));
|
|
const FName AxisScroll(TEXT("ScrollDownUp"));
|
|
const FName GISexPersons(TEXT("SexPersons"));
|
|
const FName GISexCameraIndex(TEXT("SexCameraIndex"));
|
|
const FName RoomMoveObject(TEXT("MoveObject"));
|
|
const float SexOrbitSpeed = 90.f; // degres par seconde
|
|
const float SexDollySpeed = 200.f; // cm par seconde
|
|
const float SexMinDistance = 80.f;
|
|
const float SexMaxDistance = 800.f;
|
|
const float SexLookSpeed = 90.f; // degres par seconde
|
|
// A1 (CAM2-SPEC-01 § 4.1, rev. 4) : tangage de la tete en C1 hors scene intime, a la souris (CursorY).
|
|
// CursorY est deja mis a l'echelle par la sensibilite 0,07 du mappage (DefaultInput) : 1 unite = 1 degre.
|
|
const FName AxisCursorY(TEXT("CursorY"));
|
|
const FName AxisCursorX(TEXT("CursorX"));
|
|
const float PovWitnessInterval = 1.f; // CAM2-DIAG-02 § 5 : temoin « POV souris », au plus une ligne par seconde
|
|
const float C1MouseLookScale = 1.f;
|
|
const float C1PitchMin = -60.f;
|
|
const float C1PitchMax = 70.f;
|
|
// CAM2-SPEC-01 rev. 3 § 12.2 (A1-R) : le tangage passe par la rotation de controle, que Room.UpdateRoomCam reprend
|
|
// (liaison BP 514 -> 539 et 514 -> 515). CAM2-SPEC-02 § 1 (essai COMMUN-17, « souris vers le bas va vers le haut ») :
|
|
// CursorY est positif quand la souris monte, donc +1 (le POV de PC est corrige du meme sens, broche 109.B).
|
|
const float C1MouseSign = 1.f;
|
|
const float C1MouseLogInterval = 1.f;
|
|
const float SexMaxPitch = 80.f;
|
|
// P3 bis : zoom a la molette vers le couple. Desactive : la molette garde le zoom d'origine de Room (noeud 125).
|
|
const bool bSexWheelZoom = false;
|
|
const float SexWheelStep = 40.f; // cm par cran
|
|
const float SexKeysLogInterval = 1.f;
|
|
const float SexMaxStep = 0.05f; // pas de temps maximal pris en compte (s)
|
|
|
|
// Couple a la souris (CAM-B2p-D) : en mode Scene, clic gauche maintenu sur le couple = glisser-deposer sur la
|
|
// surface visee (lit ou sol), clic droit maintenu = rotation ; la camera en vue est figee pendant la prise.
|
|
const FName PelvisSocket(TEXT("pelvis"));
|
|
const FName RoomFloor(TEXT("Box"));
|
|
const float CoupleSelectRadius = 120.f; // px, autour du segment bassin-tete projete
|
|
const float CoupleDragThreshold = 5.f; // px avant le premier deplacement
|
|
const float CoupleTurnPerPixel = 0.3f; // degres par pixel
|
|
const float CoupleFloorMargin = 30.f; // cm, marge au bord du sol de Room
|
|
const float CursorTraceLength = 5000.f;
|
|
const float CoupleTraceUp = 150.f; // depart du trace vertical au-dessus du pivot
|
|
const float CoupleTraceDown = 400.f; // fin du trace vertical sous le pivot
|
|
// G-e (CAM-B2p-H) : la racine du maillage la plus basse est posee a cette hauteur au-dessus de la surface visee.
|
|
const float CoupleRootAboveSurface = 3.f;
|
|
|
|
// Borne dure du couple (CAM-B2p-E, CAM-COMMUN9-DIAG) : pendant le sexe, a chaque image, quelle que soit la source
|
|
// du deplacement (fleches, molette, glisser, jeu). Pivot dans le sol de Room.Box moins la marge ; racine du
|
|
// maillage de chaque personne jamais sous le dessus du sol (tolerance) ni au-dessus d'un plafond estime.
|
|
const float CoupleFloorTolerance = 5.f; // cm sous le dessus du sol toleres pour la racine du maillage
|
|
const float CoupleCeilingHeight = 350.f; // cm au-dessus du sol (plafond estime : Room n'expose pas de plafond)
|
|
// CAM2-DIAG-02 § 8 (arbitrage de Big Boss) : dehors, plafond de la vue a sol + 1000 cm (WallT masque, aucun plafond trouve).
|
|
const FName RoomCeilingWallT(TEXT("WallT"));
|
|
const float OutdoorViewCeilingHeight = 1000.f;
|
|
const float CoupleHeadroom = 120.f; // cm entre la racine du maillage la plus haute et le plafond (corps assis ou allonge)
|
|
const float CoupleMoveWitness = 5.f; // cm par image hors prise au-dela desquels le temoin ecrit
|
|
const float CoupleBoundsLogInterval = 1.f;
|
|
|
|
// Interieur reel de la piece (CAM-B2p-F, CAM-COMMUN9-DIAG § 6) : Room.Box deborde des murs. Murs = composants de
|
|
// Room nommes Wall* (« bloque par Room_C_0/Wall2 », LPShowCouple) ; repli : structure AllComponents[0..5] (resizeRoom
|
|
// ne compense l'echelle qu'a partir de l'indice 6), puis demi-cote d'echelle de la racine.
|
|
const TCHAR* const WallPrefix = TEXT("Wall");
|
|
const FName RoomAllComponents(TEXT("AllComponents"));
|
|
const int32 RoomStructureCount = 6;
|
|
const float WallMinHeight = 100.f; // un mur fait plus de 100 cm de haut
|
|
const float WallThinRatio = 0.25f; // petit cote horizontal sous le quart du grand
|
|
const float CeilingMaxThickness = 50.f;
|
|
const float CeilingMinHeight = 150.f; // au-dessus du sol
|
|
const float RootHalfSizePerScale = 50.f; // demi-cote du maillage racine par unite d'echelle (estime : carre de 100 cm)
|
|
// Marge aux murs = demi-boite du couple (Show couple : etendue X=78 Y=46) : la boite ne touche jamais un mur.
|
|
const float CoupleWallMargin = 80.f;
|
|
// Pose libre (G1) : boite du couple testee a la place visee, canal Pawn, composants de Room seulement, sol et lits admis.
|
|
const FVector CoupleProbeExtent(78.0, 46.0, 50.0);
|
|
const float CoupleProbeAboveRoot = 58.f; // centre de la boite au-dessus de la racine du maillage (bas a +8 cm)
|
|
|
|
// Recadrage et suivi du couple (CAM-B2p-G) : la camera de scene vise le milieu du corps, a distance par defaut ;
|
|
// elle suit les deplacements du couple hors glisser ; C3 (variable SexC3 du PC) recadre.
|
|
const FName PCSexC3(TEXT("SexC3"));
|
|
const float RecenterDistance = 300.f;
|
|
const float RecenterHeight = 120.f;
|
|
const float RecenterTargetAboveRoot = 60.f;
|
|
const float RecenterWallMargin = 20.f;
|
|
const float FollowMaxDistance = 900.f;
|
|
// Le pivot est la place des acteurs (l'animation ne la bouge pas) : tout pas compte, meme lent (VAL B2p-G N1).
|
|
const float FollowMinStep = 0.01f;
|
|
const float RecenterMinDistance = 50.f; // cm : distance minimale a la cible apres un obstacle (VAL B2p-G N2)
|
|
// CAM2-SPEC-01 rev. 3 § 12.4 et § 12.8 : bornes de la camera de scene, partagees par le recadrage, la borne de vue
|
|
// (modificateur de camera) et CameraHeight : une seule paire de valeurs.
|
|
const float SceneFloorMargin = 30.f; // cm au-dessus du dessus du sol
|
|
const float SceneCeilingMargin = 20.f; // cm sous le plafond (trouve ou estime)
|
|
const float SceneWallMargin = 20.f; // cm devant un mur (= RecenterWallMargin)
|
|
// Liste blanche de la borne de vue : seules les vues de CameraScene (Room.SceneCamera) en scene ou en C3 de piece.
|
|
const TCHAR* const LabelScene = TEXT("Camera: Scene");
|
|
const TCHAR* const LabelC3Couple = TEXT("Camera: C3 Couple");
|
|
const TCHAR* const LabelC3RoomOld = TEXT("Camera: C3 Room (old)");
|
|
const float ViewBoundsLogInterval = 1.f;
|
|
|
|
// G-g (CAM-B2p-H) : clic molette court pendant le sexe = bascule du panneau des positions, par l'evenement
|
|
// LeftMouseSexOn du PC (le meme que la voie d'Espace). Une glissee a la molette reste l'orbite.
|
|
const FName PCLeftMouseSexOn(TEXT("LeftMouseSexOn"));
|
|
const float PanelClickMaxHold = 0.2f; // s, comme HoldTime du PC
|
|
const float PanelClickMaxMove = 5.f; // px
|
|
|
|
// Aide des controles du sexe (CAM-B2p-I) : icone « i » (texture de FAQ du menu principal) dans le panneau des actions
|
|
// (Cursor), a cote du filtre SearchAction ; les 4 lignes d'origine (SexInterface.TextBlock_1) sont rendues invisibles.
|
|
const FName GISexInterface(TEXT("SexInterface"));
|
|
const FName SexHelpLinesName(TEXT("TextBlock_1"));
|
|
const TCHAR* const SexHelpLinesStart = TEXT("Click any mouse button");
|
|
const FName PCCursorREF(TEXT("CursorREF"));
|
|
const FName CursorSearchAction(TEXT("SearchAction"));
|
|
const FName CursorBelowCursor(TEXT("BelowCursor"));
|
|
const FName HelpButtonName(TEXT("LPControlsHelp"));
|
|
const TCHAR* const HelpIconPath = TEXT("/Game/Heavy/New/info.info");
|
|
const float HelpIconSize = 28.f;
|
|
const int32 HelpZOrder = 60;
|
|
|
|
// KEY2 (KEY2-SPEC-01 § 6, § 8b, § 8c) : touche d'action (meme evenement que G-g), hauteur de la camera de scene,
|
|
// couple sur Y. Actions et axes de DefaultInput.ini, remappables dans le menu Clavier.
|
|
const FName ActionPanelAction(TEXT("ActionPanel"));
|
|
const FName AxisCameraHeight(TEXT("CameraHeight"));
|
|
const FName AxisCoupleMoveY(TEXT("CoupleMoveY"));
|
|
const float CameraHeightSpeed = 100.f; // cm par seconde (a regler a l'essai)
|
|
// Hauteur ajoutee bornee comme C2Height de Room ([-40, 150], KEY2-CART-02 § 5), depuis le dernier recadrage.
|
|
const float CameraHeightMin = -40.f;
|
|
const float CameraHeightMax = 150.f;
|
|
// Repere de CoupleMoveY (Q12, decision 14) : World = Y du monde, Couple = droite du premier acteur. Plus lu depuis CAM2-SPEC-02 § 7
|
|
// (CoupleMoveY = Z) ; repere des fleches : bCoupleKeysCameraFrame.
|
|
enum class ECoupleMoveSpace : uint8 { World, Couple };
|
|
const ECoupleMoveSpace CoupleMoveSpace = ECoupleMoveSpace::World;
|
|
const float CoupleMoveYStep = 5.f; // par unite d'axe et par image, comme le X de PC (KEY2-CART-02 § 5)
|
|
// CAM2-SPEC-02 § 7 (decision de l'utilisateur) : en scene, V / X (axe CoupleMoveY, nom garde, N1) montent et baissent le
|
|
// couple (Z) ; fleches haut / bas = Y, gauche / droite = X, dans la piece visible ou cachee (PC ne fait plus rien en scene).
|
|
// Repere monde par defaut ; vrai = repere de la camera (haut = s'eloigner de la camera, droite = droite de la camera).
|
|
const bool bCoupleKeysCameraFrame = false;
|
|
const float CoupleKeyUpSign = 1.f; // fleche Haut = +Y (repere monde)
|
|
const float CoupleKeyRightSign = 1.f; // fleche Droite = +X (repere monde)
|
|
const float CoupleOutdoorZBelow = 30.f; // piece cachee : Z du pivot au plus 30 cm sous son Z d'entree en scene
|
|
const float CoupleOutdoorZAbove = 150.f; // piece cachee : et au plus 150 cm au-dessus
|
|
// CAM2-SPEC-02 § 5 (S1-C++) : studio de creation. Vue des cameras du studio et personnage edite decales du meme vecteur
|
|
// (-d, 0, 0), sans modifier GI. d = 135 cm : places CharGen de GI.ActorTransforms a X -3131290 et -3131300 (MCP), face +X
|
|
// du panneau GI.Wall a -3131485, 50 cm de marge : d <= -3131300 + 3131435 = 135. Recentrage lateral (Y) non demande : 0.
|
|
const FName GICharacterBeingEdited(TEXT("CharacterBeingEdited"));
|
|
const FName GIActorTransforms(TEXT("ActorTransforms"));
|
|
const FVector StudioOffset(-135.0, 0.0, 0.0);
|
|
const double StudioSpotTolerance = 5.0; // cm (XY) : le personnage est sur une place CharGen
|
|
|
|
// Copie de LPDirectControlSubsystem.cpp (MenuClasses) : menus pendant lesquels le texte est masque.
|
|
const TCHAR* const MenuClasses[] = {
|
|
TEXT("Journal_C"), TEXT("MainMenu_C"), TEXT("CityMenu_C"), TEXT("StatMenu_C"),
|
|
TEXT("ContactMenu_C"), TEXT("Inventory_C"), TEXT("CharGen_C"), TEXT("ModManager_C"),
|
|
TEXT("BodyShapeMenu_C")
|
|
};
|
|
|
|
UObject* ReadObject(const UObject* Container, FName Name)
|
|
{
|
|
const FObjectPropertyBase* Prop = Container ? CastField<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);
|
|
}
|
|
|
|
// KEY2 : une touche de l'action Name vient d'etre pressee, modificateurs exacts (comme InteractPressed et
|
|
// ModifiersMatch de LPDirectControlSubsystem.cpp), mappages et remappages suivis.
|
|
bool ActionJustPressed(const APlayerController* PC, FName Action)
|
|
{
|
|
const UInputSettings* Settings = UInputSettings::GetInputSettings();
|
|
if (!PC || !PC->PlayerInput || !Settings)
|
|
{
|
|
return false;
|
|
}
|
|
const bool bCtrl = PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl);
|
|
const bool bShift = PC->IsInputKeyDown(EKeys::LeftShift) || PC->IsInputKeyDown(EKeys::RightShift);
|
|
const bool bAlt = PC->IsInputKeyDown(EKeys::LeftAlt) || PC->IsInputKeyDown(EKeys::RightAlt);
|
|
const bool bCmd = PC->IsInputKeyDown(EKeys::LeftCommand) || PC->IsInputKeyDown(EKeys::RightCommand);
|
|
for (const FInputActionKeyMapping& Map : Settings->GetActionMappings())
|
|
{
|
|
if (Map.ActionName == Action && Map.bCtrl == bCtrl && Map.bShift == bShift && Map.bAlt == bAlt && Map.bCmd == bCmd
|
|
&& PC->PlayerInput->WasJustPressed(Map.Key))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Noms des touches du clavier de l'action ou de l'axe Name, mappages en cours (comme AxisValue, remappages suivis) ;
|
|
// Sign : 1 ou -1 = echelle de l'axe de ce signe, 0 = toutes. Souris, manette et axes analogiques exclus.
|
|
FString MappedKeys(FName Name, int32 Sign)
|
|
{
|
|
TArray<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;
|
|
// Plafond de la VUE de scene : Ceiling, sauf dehors (CAM2-DIAG-02 § 8, arbitrage de Big Boss) ; le couple garde Ceiling.
|
|
double ViewCeiling = 0.0;
|
|
bool bOutdoorSky = false;
|
|
int32 Walls = 0;
|
|
bool bCeilingFound = false;
|
|
FString Source;
|
|
FBox FloorBox = FBox(ForceInit);
|
|
FVector Scale = FVector::OneVector;
|
|
};
|
|
|
|
bool ComputeRoomInterior(const AActor* Room, FRoomInterior& Out)
|
|
{
|
|
const UPrimitiveComponent* Floor = Room ? Cast<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)");
|
|
}
|
|
// 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;
|
|
}
|
|
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);
|
|
}
|
|
|
|
static void RemoveViewBounds(UGameInstance* GI);
|
|
|
|
void ULPCameraModeSubsystem::Deinitialize()
|
|
{
|
|
FTSTicker::GetCoreTicker().RemoveTicker(SwitchKeyHandle);
|
|
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
|
|
RemoveViewBounds(GetGameInstance());
|
|
RemoveWidget();
|
|
RemoveHelpBox();
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
// R-B1f (CAM2-SPEC-01 § 9.2) : au front montant de SexOn, C1Hidden repasse a faux. Une scene lancee depuis C1 ne laisse
|
|
// plus cheveux et cou masques (risque non observe, sans reponse de l'utilisateur ; traite comme non observe).
|
|
static void ClearC1HiddenOnSexEntry(UGameInstance* GI)
|
|
{
|
|
static bool bSexWasOn = false;
|
|
const bool bSexOn = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn);
|
|
if (bSexOn && !bSexWasOn)
|
|
{
|
|
if (UObject* Room = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom))
|
|
{
|
|
LPCameraModeLocal::WriteBool(Room, LPCameraModeLocal::RoomC1Hidden, false);
|
|
}
|
|
}
|
|
bSexWasOn = bSexOn;
|
|
}
|
|
|
|
// A1-R (CAM2-SPEC-01 rev. 3 § 12.2, decision de l'utilisateur : « on bouge la tete avec la souris sur tous les axes ») :
|
|
// en C1 hors scene intime, la souris (CursorY) regle le tangage de la rotation de controle. Room.UpdateRoomCam (chaque image)
|
|
// et Room.CircleCamera (lacet) reprennent ce tangage (liaisons BP 514 -> 539 et 514 -> 515) : le C++ n'ecrit plus FirstPersonSM,
|
|
// que UpdateRoomCam reecrivait avec un tangage nul (CAM2-DIAG-01). Appel a chaque image : CursorY est un delta par image.
|
|
static void TickC1Pitch(UGameInstance* GI)
|
|
{
|
|
if (!GI || LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn))
|
|
{
|
|
return;
|
|
}
|
|
UWorld* World = GI->GetWorld();
|
|
APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
|
if (!PC)
|
|
{
|
|
return;
|
|
}
|
|
const float MouseY = PC->GetInputAxisValue(LPCameraModeLocal::AxisCursorY);
|
|
if (FMath::IsNearlyZero(MouseY))
|
|
{
|
|
return;
|
|
}
|
|
const FRotator Control = PC->GetControlRotation();
|
|
const double Wanted = FMath::Clamp(FRotator::NormalizeAxis(Control.Pitch) + LPCameraModeLocal::C1MouseSign * MouseY * LPCameraModeLocal::C1MouseLookScale,
|
|
static_cast<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(""));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ULPCameraModeSubsystem::Tick(float DeltaTime)
|
|
{
|
|
// Avant les retours de l'indicateur : le filtre du panneau qui a le focus compte comme un menu pour MenuOnScreen.
|
|
TickControlsHelp();
|
|
const FString Label = ComputeLabel();
|
|
ClearC1HiddenOnSexEntry(GetGameInstance());
|
|
const bool bMenu = LPCameraModeLocal::MenuOnScreen(GetGameInstance());
|
|
bMenuOnScreen = bMenu;
|
|
RepairAfterCharGen(Label, bMenu, DeltaTime);
|
|
if (!Label.IsEmpty())
|
|
{
|
|
LastEmptyView.Reset();
|
|
}
|
|
else if (!bMenu)
|
|
{
|
|
// Temoin touche C (CAM-B2j) : vue non reconnue hors menu, une ligne par changement de vue.
|
|
const UWorld* World = GetGameInstance() ? GetGameInstance()->GetWorld() : nullptr;
|
|
const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
|
const AActor* View = PC ? PC->GetViewTarget() : nullptr;
|
|
const FString ViewText = FString::Printf(TEXT("%s, classe %s"), *GetNameSafe(View), View ? *View->GetClass()->GetName() : TEXT("-"));
|
|
if (ViewText != LastEmptyView)
|
|
{
|
|
LastEmptyView = ViewText;
|
|
ShownLabel.Reset();
|
|
UE_LOG(LogLifePlay, Log, TEXT("Indicateur de camera : aucun (vue %s)"), *ViewText);
|
|
}
|
|
}
|
|
if (Label.IsEmpty() || bMenu)
|
|
{
|
|
SetShown(false);
|
|
return true;
|
|
}
|
|
if (!EnsureWidget())
|
|
{
|
|
return true;
|
|
}
|
|
if (Label != ShownLabel)
|
|
{
|
|
Text->SetText(FText::FromString(Label));
|
|
ShownLabel = Label;
|
|
UE_LOG(LogLifePlay, Log, TEXT("Indicateur de camera : %s"), *Label);
|
|
}
|
|
SetShown(true);
|
|
if (Label == LPCameraModeLocal::LabelC2)
|
|
{
|
|
WitnessC2(DeltaTime);
|
|
WitnessOcclusion(DeltaTime);
|
|
}
|
|
else if (Label == LPCameraModeLocal::LabelC1)
|
|
{
|
|
WitnessC1(DeltaTime);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ULPCameraModeSubsystem::RepairAfterCharGen(const FString& Label, bool bMenu, float DeltaTime)
|
|
{
|
|
UGameInstance* GI = GetGameInstance();
|
|
if (!GI)
|
|
{
|
|
return;
|
|
}
|
|
if (LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GICharGenOn))
|
|
{
|
|
bCharGenSeen = true;
|
|
CharGenRepairTimeout = LPCameraModeLocal::CharGenRepairDelay;
|
|
return;
|
|
}
|
|
if (!bCharGenSeen)
|
|
{
|
|
return;
|
|
}
|
|
const UWorld* World = GI->GetWorld();
|
|
const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
|
const AActor* View = PC ? PC->GetViewTarget() : nullptr;
|
|
bool bRoomCamera = false;
|
|
for (const TCHAR* RoomLabel : LPCameraModeLocal::RoomCameraLabels)
|
|
{
|
|
bRoomCamera |= Label == RoomLabel;
|
|
}
|
|
if (!bRoomCamera || bMenu)
|
|
{
|
|
CharGenRepairTimeout -= DeltaTime;
|
|
if (CharGenRepairTimeout <= 0.f)
|
|
{
|
|
bCharGenSeen = false;
|
|
UE_LOG(LogLifePlay, Log, TEXT("CharGen : sortie sans camera de piece (vue %s, indicateur '%s'), rien remis"), *GetNameSafe(View), *Label);
|
|
}
|
|
return;
|
|
}
|
|
// Vue revenue sur une camera de piece (jamais Birdview, la carte de la ville) : une seule reparation.
|
|
bCharGenSeen = false;
|
|
if (!LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISpaceOn))
|
|
{
|
|
LPCameraModeLocal::WriteBool(GI, LPCameraModeLocal::GISpaceOn, true);
|
|
UE_LOG(LogLifePlay, Log, TEXT("CharGen : SpaceOn remis a vrai (vue %s, indicateur '%s')"), *GetNameSafe(View), *Label);
|
|
}
|
|
if (LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GIMainMenuOn) && !LPCameraModeLocal::WidgetOnScreen(GI, LPCameraModeLocal::MainMenuClass))
|
|
{
|
|
LPCameraModeLocal::WriteBool(GI, LPCameraModeLocal::GIMainMenuOn, false);
|
|
UE_LOG(LogLifePlay, Log, TEXT("CharGen : MainMenuOn remis a faux (menu principal ferme)"));
|
|
}
|
|
}
|
|
|
|
void ULPCameraModeSubsystem::WitnessC1(float DeltaTime)
|
|
{
|
|
C1Cooldown = FMath::Max(0.f, C1Cooldown - DeltaTime);
|
|
if (C1Cooldown > 0.f)
|
|
{
|
|
return;
|
|
}
|
|
C1Cooldown = LPCameraModeLocal::C1Interval;
|
|
const UGameInstance* GI = GetGameInstance();
|
|
const UWorld* World = GI ? GI->GetWorld() : nullptr;
|
|
const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
|
const ACharacter* Player = Cast<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);
|
|
// CAM2-SPEC-01 rev. 3 § 12.2 : le tangage voulu va de C1PitchMin a C1PitchMax ; le temoin ne signale qu'au-dela (marge 5).
|
|
const double CameraPitch = FRotator::NormalizeAxis(CameraRotation.Pitch);
|
|
if (Distance <= LPCameraModeLocal::C1MaxEyeDistance && CameraPitch >= LPCameraModeLocal::C1PitchMin - 5.f
|
|
&& CameraPitch <= LPCameraModeLocal::C1PitchMax + 5.f && FMath::Abs(YawGap) <= LPCameraModeLocal::C1MaxYawGap)
|
|
{
|
|
return;
|
|
}
|
|
const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom);
|
|
C1Cooldown = LPCameraModeLocal::C1LogInterval;
|
|
UE_LOG(LogLifePlay, Log, TEXT("Temoin C1 : camera a %.0f cm des yeux, tangage %+.0f, ecart de lacet %+.0f, assis=%d, minuterie=%d"),
|
|
Distance, CameraRotation.Pitch, YawGap,
|
|
IsValid(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerOccupyingFnt)) ? 1 : 0,
|
|
LPCameraModeLocal::TimerActive(IsValid(RoomObject) ? RoomObject : nullptr, LPCameraModeLocal::RoomCamTimer, World) ? 1 : 0);
|
|
}
|
|
|
|
bool ULPCameraModeSubsystem::TickSwitchKey(float DeltaTime)
|
|
{
|
|
const UGameInstance* GI = GetGameInstance();
|
|
const UWorld* World = GI ? GI->GetWorld() : nullptr;
|
|
const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
|
const UInputSettings* Settings = UInputSettings::GetInputSettings();
|
|
bool bDown = false;
|
|
if (PC && PC->PlayerInput && Settings)
|
|
{
|
|
for (const FInputActionKeyMapping& Map : Settings->GetActionMappings())
|
|
{
|
|
if (Map.ActionName == LPCameraModeLocal::SwitchCameraAction && PC->PlayerInput->IsPressed(Map.Key))
|
|
{
|
|
bDown = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (bDown && !bSwitchKeyDown)
|
|
{
|
|
LogSwitchKey(PC);
|
|
}
|
|
bSwitchKeyDown = bDown;
|
|
TickSexCamera(DeltaTime);
|
|
TickCoupleBounds(DeltaTime);
|
|
// CAM2-SPEC-01 rev. 3 : libelle calcule a chaque image (le libelle affiche suit Tick, a 0,1 s), pour que la borne de vue
|
|
// et le tangage C1 changent dans l'image meme ou la vue change.
|
|
UGameInstance* FrameGI = GetGameInstance();
|
|
const FString FrameLabel = ComputeLabel();
|
|
TickViewBounds(FrameGI, FrameLabel, bMenuOnScreen);
|
|
TickStudioFraming(FrameGI);
|
|
ResetC1PitchOnExit(FrameGI, FrameLabel == LPCameraModeLocal::LabelC1);
|
|
if (!bMenuOnScreen && FrameLabel == LPCameraModeLocal::LabelC1)
|
|
{
|
|
TickC1Pitch(FrameGI);
|
|
}
|
|
KeepBirdviewOverScene(FrameGI);
|
|
return true;
|
|
}
|
|
|
|
void ULPCameraModeSubsystem::TickCoupleBounds(float DeltaTime)
|
|
{
|
|
CoupleBoundsCooldown = FMath::Max(0.f, CoupleBoundsCooldown - DeltaTime);
|
|
CoupleMoveCooldown = FMath::Max(0.f, CoupleMoveCooldown - DeltaTime);
|
|
UGameInstance* GI = GetGameInstance();
|
|
const bool bGrabEnded = bGrabJustEnded;
|
|
bGrabJustEnded = false;
|
|
if (!GI || !LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn))
|
|
{
|
|
bHasLastPivot = false;
|
|
bHiddenRoomWarned = false;
|
|
bBoundsLogged = false;
|
|
// B2p-G : hors sexe, C3 n'a plus de sens (variable du PC remise a faux si la scene a fini en C3).
|
|
LastSexView.Reset();
|
|
bSexC3Was = false;
|
|
const UWorld* EndWorld = GI ? GI->GetWorld() : nullptr;
|
|
if (APlayerController* EndPC = EndWorld ? EndWorld->GetFirstPlayerController() : nullptr)
|
|
{
|
|
if (LPCameraModeLocal::ReadBool(EndPC, LPCameraModeLocal::PCSexC3))
|
|
{
|
|
LPCameraModeLocal::WriteBool(EndPC, LPCameraModeLocal::PCSexC3, false);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
AActor* Room = Cast<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);
|
|
}
|
|
// CAM2-SPEC-01 rev. 3 § 12.8 : bornes Z partagees avec la borne de vue et CameraHeight (memes valeurs qu'avant).
|
|
const double ZMin = Interior.FloorTop + LPCameraModeLocal::SceneFloorMargin;
|
|
Desired.Z = FMath::Clamp(Desired.Z, ZMin, FMath::Max(ZMin, Interior.ViewCeiling - LPCameraModeLocal::SceneCeilingMargin));
|
|
}
|
|
// Comme un bras de camera : devant le premier obstacle entre la cible et la camera (couple et cameras ignores).
|
|
// CAM2-SPEC-01 rev. 3 § 12.3 : F2-a retire (camRoom est la camera C2, pas un mur) ; canal ECC_Visibility d'origine.
|
|
FCollisionQueryParams Query(SCENE_QUERY_STAT(LPRecenter), true);
|
|
Query.AddIgnoredActors(Couple);
|
|
Query.AddIgnoredActor(SceneCamera);
|
|
if (AActor* Wall = Cast<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(""));
|
|
}
|
|
|
|
// CAM2-VAL-03 R2 : numero de la scene en cours, augmente a chaque debut de scene (TickSceneKeys y releve le Z d'entree du couple).
|
|
static uint32 SexSceneEntryCount = 0;
|
|
|
|
void ULPCameraModeSubsystem::TickSexCamera(float DeltaTime)
|
|
{
|
|
SexKeysCooldown = FMath::Max(0.f, SexKeysCooldown - DeltaTime);
|
|
// Pas de temps borne : un a-coup d'image (chargement) ne fait pas tourner ou avancer la camera d'un coup.
|
|
const float Step = FMath::Min(DeltaTime, LPCameraModeLocal::SexMaxStep);
|
|
UGameInstance* GI = GetGameInstance();
|
|
const bool bSexOn = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn);
|
|
// CAM-B2p-I (COMMUN-12, point 3) : debut et fin de scene ; a la fin, chaque etat touche par CAM est rendu, une ligne.
|
|
if (bSexOn != bSexStateWas)
|
|
{
|
|
bSexStateWas = bSexOn;
|
|
if (bSexOn)
|
|
{
|
|
++SexSceneEntryCount;
|
|
BeginSexScene();
|
|
}
|
|
else
|
|
{
|
|
EndSexScene();
|
|
}
|
|
}
|
|
if (!bSexOn)
|
|
{
|
|
bSceneFocusChecked = false;
|
|
bFocusClickWasDown = false;
|
|
bMiddleWasDown = false;
|
|
}
|
|
else
|
|
{
|
|
// H1 / H2 : un champ de texte (filtre du menu des positions) garde le focus clavier ; ZQSD partirait dedans.
|
|
// H1 : un clic dans la vue 3D (vu par le PlayerInput, donc hors interface) rend le focus au viewport.
|
|
// H2 : a la premiere image du sexe, un champ vide qui a le focus le rend une fois.
|
|
// CAM-B2p-H : le clic molette aussi, sinon le filtre du panneau (menu pour MenuOnScreen) le bloquerait.
|
|
const UWorld* FocusWorld = GI->GetWorld();
|
|
const APlayerController* FocusPC = FocusWorld ? FocusWorld->GetFirstPlayerController() : nullptr;
|
|
const bool bClick = FocusPC && FocusPC->PlayerInput
|
|
&& (FocusPC->PlayerInput->IsPressed(EKeys::LeftMouseButton) || FocusPC->PlayerInput->IsPressed(EKeys::RightMouseButton)
|
|
|| FocusPC->PlayerInput->IsPressed(EKeys::MiddleMouseButton));
|
|
const TSharedPtr<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));
|
|
// KEY2 (KEY2-SPEC-01 § 8c) : touche d'action, hauteur de la camera de scene, couple sur Y.
|
|
TickSceneKeys(PC, View, Room, Couple, bGrabbing, Step);
|
|
if (!bGrabbing && bKeys && Couple.Num() > 0 && View == SceneCamera)
|
|
{
|
|
// Mode Scene : la camera tourne autour du couple du meme angle que son lacet (cadrage garde, comme B2n),
|
|
// puis avance ou recule vers le couple, bornee.
|
|
const float Angle = -Right * LPCameraModeLocal::SexOrbitSpeed * Step;
|
|
FVector Offset = View->GetActorLocation() - Pivot;
|
|
Offset = Offset.RotateAngleAxis(Angle, FVector::UpVector);
|
|
// Bornes seulement quand on avance ou recule : l'orbite seule ne deplace pas une camera posee plus loin par le jeu.
|
|
double Distance = Offset.Size();
|
|
if (!FMath::IsNearlyZero(Forward) || !FMath::IsNearlyZero(Wheel))
|
|
{
|
|
Distance = FMath::Clamp(Distance - Forward * LPCameraModeLocal::SexDollySpeed * Step - Wheel * LPCameraModeLocal::SexWheelStep,
|
|
static_cast<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);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
// Temoin (KO Maj + souris, CAM-COMMUN6-DIAG § 6) : au plus une ligne par seconde tant qu'une touche est tenue.
|
|
const float Arrows = FMath::Abs(LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisLeftRight)) + FMath::Abs(LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisDownUp));
|
|
const bool bShift = PC->PlayerInput && (PC->PlayerInput->IsPressed(EKeys::LeftShift) || PC->PlayerInput->IsPressed(EKeys::RightShift));
|
|
const bool bButtons = bLeftWasDown || bRightWasDown;
|
|
if (SexKeysCooldown > 0.f || (!bKeys && FMath::IsNearlyZero(Arrows) && !bShift && !bButtons))
|
|
{
|
|
return;
|
|
}
|
|
SexKeysCooldown = LPCameraModeLocal::SexKeysLogInterval;
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : touches avant=%+.0f cote=%+.0f fleches=%+.0f/%+.0f Maj=%d MoveObject=%d MoveCamera=%d ; boutons G=%d D=%d prise=%d ; vue %s ; index POV %d ; couple %d a (%.0f, %.0f, %.0f), lacet %.0f"),
|
|
Forward, Right, LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisLeftRight), LPCameraModeLocal::AxisValue(PC, LPCameraModeLocal::AxisDownUp),
|
|
bShift ? 1 : 0, LPCameraModeLocal::ReadBool(Room, LPCameraModeLocal::RoomMoveObject) ? 1 : 0, LPCameraModeLocal::ReadBool(PC, LPCameraModeLocal::PCMoveCamera) ? 1 : 0,
|
|
bLeftWasDown ? 1 : 0, bRightWasDown ? 1 : 0, CoupleGrabMode,
|
|
*GetNameSafe(View), LPCameraModeLocal::ReadInt(GI, LPCameraModeLocal::GISexCameraIndex), Couple.Num(), Pivot.X, Pivot.Y, Pivot.Z,
|
|
Couple.Num() > 0 ? Couple[0]->GetActorRotation().Yaw : 0.0);
|
|
}
|
|
|
|
void ULPCameraModeSubsystem::TickPanelClick(APlayerController* PC, float DeltaTime)
|
|
{
|
|
// CAM-B2p-I (COMMUN-12) : Alt aussi (Espace rend sa fonction au jeu). Alt et le clic molette sont deja l'action
|
|
// RotateCamera : un appui bref sans mouvement bascule le panneau, un appui tenu ou une glissee reste l'orbite.
|
|
const bool bMiddleButton = PC->PlayerInput && PC->PlayerInput->IsPressed(EKeys::MiddleMouseButton);
|
|
const bool bAltKey = PC->PlayerInput && (PC->PlayerInput->IsPressed(EKeys::LeftAlt) || PC->PlayerInput->IsPressed(EKeys::RightAlt));
|
|
const bool bMiddle = bMiddleButton || bAltKey;
|
|
float PanelMouseX = 0.f;
|
|
float PanelMouseY = 0.f;
|
|
const bool bPanelMouse = PC->GetMousePosition(PanelMouseX, PanelMouseY);
|
|
if (bMiddle && !bMiddleWasDown)
|
|
{
|
|
bPanelByAlt = bAltKey && !bMiddleButton;
|
|
MiddleHeld = 0.f;
|
|
MiddleStart = FVector2D(PanelMouseX, PanelMouseY);
|
|
bMiddleHadMouse = bPanelMouse;
|
|
bMiddleMoved = false;
|
|
}
|
|
else if (bMiddleWasDown)
|
|
{
|
|
// Duree reelle (DeltaTime non borne) ; mouvement mesure seulement si le curseur a une position au depart et ici.
|
|
MiddleHeld += DeltaTime;
|
|
bMiddleMoved |= bMiddleHadMouse && bPanelMouse
|
|
&& FVector2D::Distance(MiddleStart, FVector2D(PanelMouseX, PanelMouseY)) > LPCameraModeLocal::PanelClickMaxMove;
|
|
if (!bMiddle && MiddleHeld <= LPCameraModeLocal::PanelClickMaxHold && !bMiddleMoved)
|
|
{
|
|
UFunction* Toggle = PC->FindFunction(LPCameraModeLocal::PCLeftMouseSexOn);
|
|
if (Toggle && Toggle->NumParms == 0)
|
|
{
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : panneau bascule (%s, %.2f s)"), bPanelByAlt ? TEXT("Alt") : TEXT("clic molette"), MiddleHeld);
|
|
PC->ProcessEvent(Toggle, nullptr);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : panneau non bascule, evenement LeftMouseSexOn absent du PC"));
|
|
}
|
|
}
|
|
}
|
|
bMiddleWasDown = bMiddle;
|
|
}
|
|
|
|
void ULPCameraModeSubsystem::TickSceneKeys(APlayerController* PC, AActor* View, const UObject* Room, const TArray<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);
|
|
}
|
|
|
|
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));
|
|
// 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;
|
|
}
|
|
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;
|
|
FVector Aim;
|
|
GrabOffset = TraceScreen(GrabMouseStart, Aim) ? FVector(Pivot.X - Aim.X, Pivot.Y - Aim.Y, 0.0) : FVector::ZeroVector;
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple pris (%s) a (%.0f, %.0f, %.0f), racine a %s de la surface %s"),
|
|
CoupleGrabMode == 1 ? TEXT("deplacement") : TEXT("rotation"), Pivot.X, Pivot.Y, Pivot.Z,
|
|
bGrabHasHeight ? *FString::Printf(TEXT("%+.0f cm"), GrabHeight) : TEXT("? cm"),
|
|
!bGrabHasHeight ? TEXT("inconnue") : GrabSurface ? *LPCameraModeLocal::DescribeComponent(GrabSurface) : TEXT("sol"));
|
|
}
|
|
|
|
const bool bHeld = CoupleGrabMode == 1 ? bLeft : bRight;
|
|
if (!bHeld)
|
|
{
|
|
const FRotator Rotation = Couple[0]->GetActorRotation();
|
|
const double CursorTravel = FVector2D::Distance(GrabMouseStart, GrabCursor);
|
|
if (CoupleGrabMode == 1)
|
|
{
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple pose a (%.0f, %.0f, %.0f), lacet %.0f"), Pivot.X, Pivot.Y, Pivot.Z, Rotation.Yaw);
|
|
if (!bGrabMoved)
|
|
{
|
|
// COMMUN-12 : glisser sans effet rendu visible (curseur reste sous le seuil, ou souris verrouillee).
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : glisser sans effet, curseur deplace de %.0f px (seuil %.0f), deltas bruts sur %d images, curseur %s"),
|
|
CursorTravel, LPCameraModeLocal::CoupleDragThreshold, GrabRawFrames, bMouse ? TEXT("avec position") : TEXT("sans position"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : couple tourne de %+.0f degres, lacet %.0f"), GrabTurned, Rotation.Yaw);
|
|
if (FMath::Abs(GrabTurned) < 0.5f)
|
|
{
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : rotation sans effet, curseur deplace de %.0f px, deltas bruts sur %d images, curseur %s"),
|
|
CursorTravel, GrabRawFrames, bMouse ? TEXT("avec position") : TEXT("sans position"));
|
|
}
|
|
}
|
|
// G3 (STE) : une seule fois par pose, si le meneur a ete deplace par la prise (glisser, ou rotation autour d'un pivot
|
|
// hors du meneur) : le resolveur de LPShowCouple reprend sa place sure ici au lieu de balayer depuis l'ancienne.
|
|
const double LeadMoved = FVector::Dist(Couple[0]->GetActorLocation(), GrabLeadStart);
|
|
if (LeadMoved > 1.0)
|
|
{
|
|
ULPShowCoupleSubsystem* ShowCouple = GI->GetSubsystem<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;
|
|
FVector Aim;
|
|
const bool bAim = bGrabMoved && TraceScreen(GrabCursor, Aim);
|
|
if (bGrabMoved && !bAim && !bGrabAimWarned)
|
|
{
|
|
bGrabAimWarned = true;
|
|
UE_LOG(LogLifePlay, Log, TEXT("Sexe : glisser, le curseur (%.0f, %.0f) ne vise aucune surface"), GrabCursor.X, GrabCursor.Y);
|
|
}
|
|
if (bAim)
|
|
{
|
|
FVector Target(Aim.X + GrabOffset.X, Aim.Y + GrabOffset.Y, Pivot.Z);
|
|
// F3 : meme interieur que E1 (murs reels, marge = demi-boite du couple).
|
|
LPCameraModeLocal::FRoomInterior Interior;
|
|
if (bFloorKnown && LPCameraModeLocal::ComputeRoomInterior(RoomActor, Interior))
|
|
{
|
|
const FBox2D& Area = Interior.Area;
|
|
const double Margin = LPCameraModeLocal::CoupleWallMargin;
|
|
if (Area.Max.X - Area.Min.X > 2.0 * Margin && Area.Max.Y - Area.Min.Y > 2.0 * Margin)
|
|
{
|
|
Target.X = FMath::Clamp(Target.X, Area.Min.X + Margin, Area.Max.X - Margin);
|
|
Target.Y = FMath::Clamp(Target.Y, Area.Min.Y + Margin, Area.Max.Y - Margin);
|
|
}
|
|
}
|
|
// G-e : la racine du maillage la plus basse est posee sur la surface sous la nouvelle place (dessus du lit ou
|
|
// sol), plus 3 cm ; elle ne depend plus de l'ecart pris a la selection (un couple qui flottait retombe).
|
|
double SurfaceZ = 0.0;
|
|
const UPrimitiveComponent* Surface = nullptr;
|
|
const bool bSurface = TraceDown(Target, SurfaceZ, Surface);
|
|
if (bSurface)
|
|
{
|
|
Target.Z = Pivot.Z + (SurfaceZ + LPCameraModeLocal::CoupleRootAboveSurface - LPCameraModeLocal::CoupleLowestRoot(Couple));
|
|
}
|
|
// G1 : place libre seulement (boite du couple sans meuble ni mur, lits admis). Sinon le couple reste a la
|
|
// derniere place libre ; une prise qui part d'une place encombree peut aller vers toute place libre.
|
|
const ACharacter* LeadCharacter = Cast<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"
|
|
" %s / %s: raise / lower the camera\n"
|
|
" Hold a mouse button away from the couple and move the mouse: rotate the camera\n"
|
|
"COUPLE\n"
|
|
" Left-drag the couple: move it (floor or bed only; off in Arrows only mode)\n"
|
|
" Right-drag the couple: turn it\n"
|
|
" %s / %s: couple up / down (raise / lower) Mouse wheel: raise / lower the couple\n"
|
|
" %s: move the couple forward / back (Y) %s: move the couple right / left (X)\n"
|
|
"ACTIONS\n"
|
|
" %s, or Alt / middle click (short press): show / hide the action panel"),
|
|
*MappedKeys(LPCameraModeLocal::SwitchCameraAction, 0),
|
|
*MappedKeys(LPCameraModeLocal::AxisMoveForward, 1), *MappedKeys(LPCameraModeLocal::AxisMoveForward, -1),
|
|
*MappedKeys(LPCameraModeLocal::AxisMoveRight, -1), *MappedKeys(LPCameraModeLocal::AxisMoveRight, 1),
|
|
*MappedKeys(LPCameraModeLocal::AxisCameraHeight, 1), *MappedKeys(LPCameraModeLocal::AxisCameraHeight, -1),
|
|
*MappedKeys(LPCameraModeLocal::AxisCoupleMoveY, 1), *MappedKeys(LPCameraModeLocal::AxisCoupleMoveY, -1),
|
|
*MappedKeys(LPCameraModeLocal::AxisDownUp, 0), *MappedKeys(LPCameraModeLocal::AxisLeftRight, 0),
|
|
*MappedKeys(LPCameraModeLocal::ActionPanelAction, 0));
|
|
}
|
|
|
|
void ULPCameraModeSubsystem::TickControlsHelp()
|
|
{
|
|
UGameInstance* GI = GetGameInstance();
|
|
const bool bSexOn = GI && LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn);
|
|
const UWorld* World = GI ? GI->GetWorld() : nullptr;
|
|
const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
|
|
|
// I-d : les 4 lignes d'origine sont rendues invisibles (opacite 0) seulement quand le bloc porte ce texte. La liaison
|
|
// Get_TextBlock_1_Text reste en place (SetText la retirerait) : un autre texte du meme bloc reste affiche.
|
|
const UUserWidget* SexUI = bSexOn ? Cast<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();
|
|
}
|