1029 lines
37 KiB
C++
1029 lines
37 KiB
C++
|
|
// Lot STE, fiches ORG5-I1 et ORG5-I2a (affichage) - voir LPBonusRewardSubsystem.h.
|
||
|
|
// Origine : comportement reconstruit d'apres LifePlay 2023.04 de Vinfamy. Licence MIT
|
||
|
|
// (SPDX-License-Identifier: MIT), texte de reference : Licenses/Vinfamy_License.txt.
|
||
|
|
|
||
|
|
#include "LPBonusRewardSubsystem.h"
|
||
|
|
|
||
|
|
#include "Blueprint/UserWidget.h"
|
||
|
|
#include "Blueprint/WidgetTree.h"
|
||
|
|
#include "Components/CanvasPanelSlot.h"
|
||
|
|
#include "Components/PanelWidget.h"
|
||
|
|
#include "Components/TextBlock.h"
|
||
|
|
#include "Components/VerticalBox.h"
|
||
|
|
#include "Components/VerticalBoxSlot.h"
|
||
|
|
#include "Engine/GameInstance.h"
|
||
|
|
#include "HAL/PlatformTime.h"
|
||
|
|
#include "Kismet/KismetMathLibrary.h"
|
||
|
|
#include "LifePlay.h"
|
||
|
|
#include "Styling/CoreStyle.h"
|
||
|
|
#include "UObject/TextProperty.h"
|
||
|
|
#include "UObject/UnrealType.h"
|
||
|
|
|
||
|
|
namespace LPBonusReward
|
||
|
|
{
|
||
|
|
constexpr float PollSeconds = 0.1f; // sondage H1 (STE-ORG5-SPEC.md 2.2)
|
||
|
|
constexpr double DebounceSeconds = 0.2; // RetriggerableDelay(0.2) de l'auteur (SPEC 1.1 E3)
|
||
|
|
constexpr int32 CandidateCount = 10; // Array_Resize(10) apres Array_Shuffle (SPEC 1.1 E5)
|
||
|
|
constexpr int32 MaxRewards = 3; // NotEqual_IntInt(countBR, 3) (SPEC 1.1 E6e)
|
||
|
|
const TCHAR* const AttractionKey = TEXT("attractiontoplayer");
|
||
|
|
|
||
|
|
// Affichage (fiche ORG5-I2a, STE-ORG5-I2-SPEC.md 2.2 et decisions de l'utilisateur).
|
||
|
|
constexpr double RewardLineSeconds = 20.0; // Q-I2-2 : chaque ligne retiree 20 s apres son affichage
|
||
|
|
constexpr int32 MaxRewardLines = 1; // V-min : une ligne, remplacee a la recompense suivante (SPEC 2.2, V6-3)
|
||
|
|
constexpr double EquipMenuMinHeight = 500.0; // Q-I2-3 : hauteur d'EquipMenu chez l'auteur (SPEC 1.1 A3)
|
||
|
|
constexpr float RewardFontSize = 16.f; // RewardText de l'auteur (SPEC 1.1 A5)
|
||
|
|
constexpr int32 RewardOutlineSize = 2; // idem, contour noir alpha 0,8
|
||
|
|
const TCHAR* const CursorProp = TEXT("Cursor");
|
||
|
|
const TCHAR* const EquipMenuName = TEXT("EquipMenu");
|
||
|
|
const TCHAR* const ActionTextName = TEXT("ActionText");
|
||
|
|
const TCHAR* const RewardBoxName = TEXT("LPBonusRewardBox");
|
||
|
|
|
||
|
|
// Retire une ligne de l'ecran si elle existe encore (pas d'acces apres destruction).
|
||
|
|
void RemoveLine(const FLPBonusRewardLine& Entry)
|
||
|
|
{
|
||
|
|
UTextBlock* Line = Entry.Line.Get();
|
||
|
|
if (IsValid(Line))
|
||
|
|
{
|
||
|
|
Line->RemoveFromParent();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Police de la ligne : copiee d'ActionText (texte voisin dans EquipMenu, SPEC 1.4 N1), comme
|
||
|
|
// LPColorGradeSubsystem.cpp:295, sinon police par defaut de Slate ; puis taille 16 et contour 2 noir 0,8 de
|
||
|
|
// l'auteur (SPEC 1.1 A5). FSlateFontInfo::Size et OutlineSettings : SlateFontInfo.h:175, :160 ;
|
||
|
|
// FFontOutlineSettings(int32, FLinearColor) : SlateFontInfo.h:78.
|
||
|
|
FSlateFontInfo RewardFont(const UUserWidget* Cursor)
|
||
|
|
{
|
||
|
|
const UTextBlock* Sample = Cast<UTextBlock>(Cursor->GetWidgetFromName(FName(ActionTextName)));
|
||
|
|
FSlateFontInfo Font = Sample ? Sample->GetFont() : FCoreStyle::GetDefaultFontStyle("Regular", RewardFontSize);
|
||
|
|
Font.Size = RewardFontSize;
|
||
|
|
Font.OutlineSettings = FFontOutlineSettings(RewardOutlineSize, FLinearColor(0.f, 0.f, 0.f, 0.8f));
|
||
|
|
return Font;
|
||
|
|
}
|
||
|
|
|
||
|
|
FProperty* FindProp(const UObject* Owner, const TCHAR* Name)
|
||
|
|
{
|
||
|
|
return Owner ? Owner->GetClass()->FindPropertyByName(FName(Name)) : nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parametre d'une fonction Blueprint, par nom (comme LPDirectControlSubsystem.cpp:375-385).
|
||
|
|
FProperty* FindParm(UFunction* Function, const TCHAR* Name)
|
||
|
|
{
|
||
|
|
for (TFieldIterator<FProperty> It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It)
|
||
|
|
{
|
||
|
|
if (It->GetAuthoredName() == Name || It->GetName() == Name)
|
||
|
|
{
|
||
|
|
return *It;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Liste lisible des parametres, pour l'avertissement unique quand la signature n'est pas celle attendue.
|
||
|
|
FString DescribeParms(UFunction* Function)
|
||
|
|
{
|
||
|
|
FString Out;
|
||
|
|
for (TFieldIterator<FProperty> It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It)
|
||
|
|
{
|
||
|
|
if (!Out.IsEmpty())
|
||
|
|
{
|
||
|
|
Out += TEXT(", ");
|
||
|
|
}
|
||
|
|
Out += FString::Printf(TEXT("%s (%s%s)"), *It->GetAuthoredName(), *It->GetClass()->GetName(),
|
||
|
|
It->HasAnyPropertyFlags(CPF_OutParm) ? TEXT(", sortie") : TEXT(""));
|
||
|
|
}
|
||
|
|
return Out;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reel Blueprint : double en 5.x (FDoubleProperty), float accepte aussi (comme LPUserSettings.cpp:32-55).
|
||
|
|
bool IsReal(const FProperty* Prop)
|
||
|
|
{
|
||
|
|
return CastField<FDoubleProperty>(Prop) || CastField<FFloatProperty>(Prop);
|
||
|
|
}
|
||
|
|
|
||
|
|
double ReadReal(const FProperty* Prop, const void* Container)
|
||
|
|
{
|
||
|
|
if (const FDoubleProperty* AsDouble = CastField<FDoubleProperty>(Prop))
|
||
|
|
{
|
||
|
|
return AsDouble->GetPropertyValue_InContainer(Container);
|
||
|
|
}
|
||
|
|
if (const FFloatProperty* AsFloat = CastField<FFloatProperty>(Prop))
|
||
|
|
{
|
||
|
|
return static_cast<double>(AsFloat->GetPropertyValue_InContainer(Container));
|
||
|
|
}
|
||
|
|
return 0.0;
|
||
|
|
}
|
||
|
|
|
||
|
|
void WriteReal(const FProperty* Prop, void* Container, double Value)
|
||
|
|
{
|
||
|
|
if (const FDoubleProperty* AsDouble = CastField<FDoubleProperty>(Prop))
|
||
|
|
{
|
||
|
|
AsDouble->SetPropertyValue_InContainer(Container, Value);
|
||
|
|
}
|
||
|
|
else if (const FFloatProperty* AsFloat = CastField<FFloatProperty>(Prop))
|
||
|
|
{
|
||
|
|
AsFloat->SetPropertyValue_InContainer(Container, static_cast<float>(Value));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Booleen d'entree d'une fonction : present et booleen, sinon faux (l'appelant avertit et abandonne).
|
||
|
|
bool SetBoolParm(UFunction* Function, void* Parms, const TCHAR* Name, bool bValue)
|
||
|
|
{
|
||
|
|
const FBoolProperty* Prop = CastField<FBoolProperty>(FindParm(Function, Name));
|
||
|
|
if (!Prop)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
Prop->SetPropertyValue_InContainer(Parms, bValue);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parametres d'un appel par ProcessEvent : memoire fournie par l'appelant (FMemory_Alloca_Aligned, qui doit rester
|
||
|
|
// dans le cadre de l'appelant), mise a zero et initialisee ici, detruite a la sortie de portee
|
||
|
|
// (meme sequence que LPDirectControlSubsystem.cpp:426-455).
|
||
|
|
struct FScopedParms
|
||
|
|
{
|
||
|
|
UFunction* Function;
|
||
|
|
uint8* Memory;
|
||
|
|
|
||
|
|
FScopedParms(UFunction* InFunction, uint8* InMemory)
|
||
|
|
: Function(InFunction)
|
||
|
|
, Memory(InMemory)
|
||
|
|
{
|
||
|
|
FMemory::Memzero(Memory, Function->ParmsSize);
|
||
|
|
for (TFieldIterator<FProperty> It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It)
|
||
|
|
{
|
||
|
|
It->InitializeValue_InContainer(Memory);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
~FScopedParms()
|
||
|
|
{
|
||
|
|
for (TFieldIterator<FProperty> It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It)
|
||
|
|
{
|
||
|
|
It->DestroyValue_InContainer(Memory);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FScopedParms(const FScopedParms&) = delete;
|
||
|
|
FScopedParms& operator=(const FScopedParms&) = delete;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Proprietes lues par reflexion. Faux : propriete absente ou d'un autre type (l'appelant avertit).
|
||
|
|
bool ReadObject(const UObject* Owner, const TCHAR* Name, UObject*& Out)
|
||
|
|
{
|
||
|
|
Out = nullptr;
|
||
|
|
const FObjectPropertyBase* Prop = CastField<FObjectPropertyBase>(FindProp(Owner, Name));
|
||
|
|
if (!Prop)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
Out = Prop->GetObjectPropertyValue_InContainer(Owner);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Tableau d'objets (comme LPShowCoupleSubsystem.cpp:80-96).
|
||
|
|
bool ReadObjects(const UObject* Owner, const TCHAR* Name, TArray<UObject*>& Out)
|
||
|
|
{
|
||
|
|
Out.Reset();
|
||
|
|
const FArrayProperty* ArrayProp = CastField<FArrayProperty>(FindProp(Owner, Name));
|
||
|
|
const FObjectPropertyBase* Inner = ArrayProp ? CastField<FObjectPropertyBase>(ArrayProp->Inner) : nullptr;
|
||
|
|
if (!Inner)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr<void>(Owner));
|
||
|
|
for (int32 Index = 0; Index < Helper.Num(); ++Index)
|
||
|
|
{
|
||
|
|
Out.Add(Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index)));
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Tableau de chaines (GI.CurrentTags). D2 (STE-ORG5-V1-VAL.md) : type reel non lu, elements chaine, nom ou texte
|
||
|
|
// acceptes et convertis en chaines.
|
||
|
|
bool ReadStrings(const UObject* Owner, const TCHAR* Name, TArray<FString>& Out)
|
||
|
|
{
|
||
|
|
Out.Reset();
|
||
|
|
const FArrayProperty* ArrayProp = CastField<FArrayProperty>(FindProp(Owner, Name));
|
||
|
|
if (!ArrayProp)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const FStrProperty* AsStr = CastField<FStrProperty>(ArrayProp->Inner);
|
||
|
|
const FNameProperty* AsName = CastField<FNameProperty>(ArrayProp->Inner);
|
||
|
|
const FTextProperty* AsText = CastField<FTextProperty>(ArrayProp->Inner);
|
||
|
|
if (!AsStr && !AsName && !AsText)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr<void>(Owner));
|
||
|
|
Out.Reserve(Helper.Num());
|
||
|
|
for (int32 Index = 0; Index < Helper.Num(); ++Index)
|
||
|
|
{
|
||
|
|
const void* Item = Helper.GetRawPtr(Index);
|
||
|
|
if (AsStr)
|
||
|
|
{
|
||
|
|
Out.Add(AsStr->GetPropertyValue(Item));
|
||
|
|
}
|
||
|
|
else if (AsName)
|
||
|
|
{
|
||
|
|
Out.Add(AsName->GetPropertyValue(Item).ToString());
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Out.Add(AsText->GetPropertyValue(Item).ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||
|
|
{
|
||
|
|
Super::Initialize(Collection);
|
||
|
|
Rng.GenerateNewSeed();
|
||
|
|
|
||
|
|
FString Failure;
|
||
|
|
if (CheckFormula(Failure))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : contrôle formule OK"));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Error, TEXT("Bonus : contrôle formule en erreur, %s"), *Failure);
|
||
|
|
}
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : réglage bDisabled = %s ([/Script/LifePlay.LPBonusRewardSettings], config Game)"),
|
||
|
|
GetDefault<ULPBonusRewardSettings>()->bDisabled ? TEXT("vrai") : TEXT("faux"));
|
||
|
|
|
||
|
|
TickHandle = FTSTicker::GetCoreTicker().AddTicker(
|
||
|
|
FTickerDelegate::CreateUObject(this, &ULPBonusRewardSubsystem::Tick), LPBonusReward::PollSeconds);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::Deinitialize()
|
||
|
|
{
|
||
|
|
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
|
||
|
|
Pending.Reset();
|
||
|
|
LastCounts.Reset();
|
||
|
|
// Garde 3 : minuteries des lignes videes ; les widgets ne sont pas touches ici (ils partent avec le monde et le
|
||
|
|
// GameInstance en cours de fermeture).
|
||
|
|
ClearRewardDisplay(false);
|
||
|
|
RewardBox.Reset();
|
||
|
|
Super::Deinitialize();
|
||
|
|
}
|
||
|
|
|
||
|
|
const TArray<FLPBonusStat>& ULPBonusRewardSubsystem::Table()
|
||
|
|
{
|
||
|
|
// STE-ORG5-SPEC.md section 1.2 (valeur par defaut de Cursor.statTags chez l'auteur), meme ordre.
|
||
|
|
static const TArray<FLPBonusStat> Stats = {
|
||
|
|
{ TEXT("perversion"), {} },
|
||
|
|
{ TEXT("masochist"), { TEXT("Aggressive"), TEXT("AggressiveFM") } },
|
||
|
|
{ TEXT("prone_to_orgasm"), {} },
|
||
|
|
{ TEXT("attractiontoplayer"), {} },
|
||
|
|
{ TEXT("fitness"), { TEXT("Penetration"), TEXT("Foreplay"), TEXT("Cum") } },
|
||
|
|
{ TEXT("likes_anal"), { TEXT("Anal") } },
|
||
|
|
{ TEXT("likes_boobjob"), { TEXT("Titfuck"), TEXT("Boobjob") } },
|
||
|
|
{ TEXT("likes_cowgirl"), { TEXT("Cow"), TEXT("Cowgirl") } },
|
||
|
|
{ TEXT("likes_doggy"), { TEXT("Doggy"), TEXT("Doggystyle") } },
|
||
|
|
{ TEXT("likes_handjob"), { TEXT("Handjob"), TEXT("RubPussy"), TEXT("FingerPussy") } },
|
||
|
|
{ TEXT("likes_missionary"), { TEXT("Missionary") } },
|
||
|
|
{ TEXT("likes_oral"), { TEXT("Oral"), TEXT("Blowjob"), TEXT("Cunnilingus"), TEXT("LickPussy"), TEXT("LickAss"), TEXT("Rimjob") } },
|
||
|
|
{ TEXT("likes_rcowgirl"), { TEXT("ReverseCow"), TEXT("ReverseCowgirl") } },
|
||
|
|
{ TEXT("likes_rough"), { TEXT("Aggressive"), TEXT("AggressiveFM") } },
|
||
|
|
{ TEXT("likes_sidefuck"), { TEXT("Spooning"), TEXT("Sidefuck"), TEXT("Spoon") } },
|
||
|
|
{ TEXT("likes_toys"), { TEXT("Toys"), TEXT("Dildo"), TEXT("Vibrator") } },
|
||
|
|
{ TEXT("likes_tribadism"), { TEXT("Tribadism"), TEXT("Scissor"), TEXT("Scissoring") } },
|
||
|
|
{ TEXT("likes_vaginal"), { TEXT("Vaginal") } },
|
||
|
|
{ TEXT("masochist-"), { TEXT("Aggressive"), TEXT("AggressiveFM") } },
|
||
|
|
{ TEXT("prone_to_orgasm-"), { TEXT("Penetration"), TEXT("Foreplay"), TEXT("Cum") } },
|
||
|
|
};
|
||
|
|
return Stats;
|
||
|
|
}
|
||
|
|
|
||
|
|
double ULPBonusRewardSubsystem::RewardChange(double X, bool bNegative)
|
||
|
|
{
|
||
|
|
// Ordre de l'auteur (SPEC 1.3) : Abs, MapRangeClamped(0, 100, 0, 1.9), 2 - m, * 10, FCeil, entier -> reel, / 10,
|
||
|
|
// * signe (SelectFloat(-1, 1, EndsWith("-"))). Memes fonctions que les noeuds Blueprint (KismetMathLibrary.h:796,
|
||
|
|
// :836), en double comme les Blueprints 5.x.
|
||
|
|
const double Mapped = UKismetMathLibrary::MapRangeClamped(FMath::Abs(X), 0.0, 100.0, 0.0, 1.9);
|
||
|
|
const int32 Tenths = UKismetMathLibrary::FCeil((2.0 - Mapped) * 10.0);
|
||
|
|
const double Value = static_cast<double>(Tenths) / 10.0;
|
||
|
|
return Value * (bNegative ? -1.0 : 1.0);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::CheckFormula(FString& OutFailure)
|
||
|
|
{
|
||
|
|
struct FCase
|
||
|
|
{
|
||
|
|
double X;
|
||
|
|
bool bNegative;
|
||
|
|
double Expected;
|
||
|
|
};
|
||
|
|
// SPEC 2.4 : 0, 10, 25, 50, 75, 90, 100 -> 2.0, 1.9, 1.6, 1.1, 0.6, 0.3, 0.2 ; plus le signe d'une cle en '-'.
|
||
|
|
static const FCase Cases[] = {
|
||
|
|
{ 0.0, false, 2.0 }, { 10.0, false, 1.9 }, { 25.0, false, 1.6 }, { 50.0, false, 1.1 },
|
||
|
|
{ 75.0, false, 0.6 }, { 90.0, false, 0.3 }, { 100.0, false, 0.2 }, { 50.0, true, -1.1 },
|
||
|
|
};
|
||
|
|
for (const FCase& Case : Cases)
|
||
|
|
{
|
||
|
|
const double Got = RewardChange(Case.X, Case.bNegative);
|
||
|
|
if (!FMath::IsNearlyEqual(Got, Case.Expected, 1e-9))
|
||
|
|
{
|
||
|
|
OutFailure = FString::Printf(TEXT("x=%.1f%s : %.17g au lieu de %.1f"),
|
||
|
|
Case.X, Case.bNegative ? TEXT(" (baisse)") : TEXT(""), Got, Case.Expected);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::Overlaps(const TArray<FString>& A, const TArray<FString>& B, bool bIfEmptyThenTrue)
|
||
|
|
{
|
||
|
|
// GI.Overlaps (L6f_dump_GI.txt:19622-19673) : IfEmptyThenTrue ET (A vide OU B vide) -> vrai ; sinon un element de
|
||
|
|
// A contenu dans B (Array_Contains) -> vrai ; sinon faux.
|
||
|
|
if (bIfEmptyThenTrue && (A.Num() == 0 || B.Num() == 0))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
for (const FString& Item : A)
|
||
|
|
{
|
||
|
|
if (B.Contains(Item))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::Pick(const TArray<FString>& PositionTags, FRandomStream& InRng, bool bAttractionAllowed,
|
||
|
|
TArray<FString>& OutStats, TArray<bool>& OutNegative)
|
||
|
|
{
|
||
|
|
OutStats.Reset();
|
||
|
|
OutNegative.Reset();
|
||
|
|
const TArray<FLPBonusStat>& Stats = Table();
|
||
|
|
|
||
|
|
// Map_Keys(statTags) puis Array_Shuffle : meme algorithme que KismetArrayLibrary.cpp:338-352 (ici par le flux
|
||
|
|
// InRng, graine injectable), puis Array_Resize(10).
|
||
|
|
TArray<int32> Order;
|
||
|
|
Order.Reserve(Stats.Num());
|
||
|
|
for (int32 Index = 0; Index < Stats.Num(); ++Index)
|
||
|
|
{
|
||
|
|
Order.Add(Index);
|
||
|
|
}
|
||
|
|
const int32 LastIndex = Order.Num() - 1;
|
||
|
|
for (int32 i = 0; i <= LastIndex; ++i)
|
||
|
|
{
|
||
|
|
const int32 Index = InRng.RandRange(i, LastIndex);
|
||
|
|
if (i != Index)
|
||
|
|
{
|
||
|
|
Order.Swap(i, Index);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Order.SetNum(FMath::Min(Order.Num(), LPBonusReward::CandidateCount));
|
||
|
|
|
||
|
|
for (const int32 Index : Order)
|
||
|
|
{
|
||
|
|
const FLPBonusStat& Entry = Stats[Index];
|
||
|
|
if (!Overlaps(PositionTags, Entry.Tags, true))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (Entry.Key == LPBonusReward::AttractionKey && !bAttractionAllowed)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const bool bNegative = Entry.Key.EndsWith(TEXT("-"));
|
||
|
|
OutStats.Add(bNegative ? Entry.Key.LeftChop(1) : Entry.Key);
|
||
|
|
OutNegative.Add(bNegative);
|
||
|
|
if (OutStats.Num() == LPBonusReward::MaxRewards)
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::Tick(float DeltaTime)
|
||
|
|
{
|
||
|
|
(void)DeltaTime;
|
||
|
|
if (UGameInstance* GI = GetGameInstance())
|
||
|
|
{
|
||
|
|
Poll(GI);
|
||
|
|
}
|
||
|
|
FirePending();
|
||
|
|
ExpireRewardLines();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::Poll(UObject* GI)
|
||
|
|
{
|
||
|
|
const FBoolProperty* SexOn = CastField<FBoolProperty>(LPBonusReward::FindProp(GI, TEXT("SexOn")));
|
||
|
|
if (!SexOn)
|
||
|
|
{
|
||
|
|
// D1 (STE-ORG5-V1-VAL.md) : message construit seulement au premier passage (Poll tourne toutes les 0,1 s).
|
||
|
|
if (FirstWarning(TEXT("GI.SexOn")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de propriete booleenne SexOn dans %s, aucun orgasme detecte"),
|
||
|
|
*GI->GetClass()->GetName());
|
||
|
|
}
|
||
|
|
bInScene = false;
|
||
|
|
ClearRewardDisplay(true);
|
||
|
|
LastCounts.Reset();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
bInScene = SexOn->GetPropertyValue_InContainer(GI);
|
||
|
|
if (!bInScene)
|
||
|
|
{
|
||
|
|
// Hors scene : memoire des compteurs videe. Un orgasme deja detecte (Pending) garde son tempo, comme le
|
||
|
|
// RetriggerableDelay de l'auteur qui ne depend pas de la scene. Garde 3 (ORG5-I2a) : lignes de recompense
|
||
|
|
// retirees de l'ecran et minuteries videes a la sortie de scene.
|
||
|
|
LastCounts.Reset();
|
||
|
|
ClearRewardDisplay(true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
TArray<UObject*> Persons;
|
||
|
|
if (!LPBonusReward::ReadObjects(GI, TEXT("SexPersons"), Persons))
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("GI.SexPersons")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de tableau d'objets SexPersons dans %s, aucun orgasme detecte"),
|
||
|
|
*GI->GetClass()->GetName());
|
||
|
|
}
|
||
|
|
LastCounts.Reset();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
TSet<TWeakObjectPtr<UObject>> Seen;
|
||
|
|
for (UObject* Person : Persons)
|
||
|
|
{
|
||
|
|
if (!IsValid(Person))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const FIntProperty* CountProp = CastField<FIntProperty>(LPBonusReward::FindProp(Person, TEXT("Sex_OrgasmCounts")));
|
||
|
|
if (!CountProp)
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("Person.Sex_OrgasmCounts")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de propriete entiere Sex_OrgasmCounts dans %s, personne ignoree"),
|
||
|
|
*Person->GetClass()->GetName());
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const int32 Count = CountProp->GetPropertyValue_InContainer(Person);
|
||
|
|
const TWeakObjectPtr<UObject> Key(Person);
|
||
|
|
Seen.Add(Key);
|
||
|
|
|
||
|
|
int32* Last = LastCounts.Find(Key);
|
||
|
|
if (!Last)
|
||
|
|
{
|
||
|
|
// Premier passage (debut de scene, personne qui entre) : memorisation sans recompense.
|
||
|
|
LastCounts.Add(Key, Count);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (Count > *Last)
|
||
|
|
{
|
||
|
|
TArray<FString> Tags;
|
||
|
|
if (!LPBonusReward::ReadStrings(GI, TEXT("CurrentTags"), Tags))
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("GI.CurrentTags"), FString::Printf(TEXT("pas de tableau de chaines CurrentTags dans %s, orgasme sans recompense"),
|
||
|
|
*GI->GetClass()->GetName()));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (Pending.IsSet())
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : orgasme en attente remplace par celui de %s (tempo %.1f s)"),
|
||
|
|
*PersonName(Person), LPBonusReward::DebounceSeconds);
|
||
|
|
}
|
||
|
|
FLPBonusPending NewPending;
|
||
|
|
NewPending.Person = Person;
|
||
|
|
NewPending.Tags = MoveTemp(Tags);
|
||
|
|
NewPending.Count = Count;
|
||
|
|
NewPending.FireAt = FPlatformTime::Seconds() + LPBonusReward::DebounceSeconds;
|
||
|
|
Pending = MoveTemp(NewPending);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Hausse : nouveau compteur retenu ; baisse (remise a zero du jeu) : resynchronisation sans recompense.
|
||
|
|
*Last = Count;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Personne sortie de la scene : oubliee (si elle revient, premier passage sans recompense).
|
||
|
|
for (auto It = LastCounts.CreateIterator(); It; ++It)
|
||
|
|
{
|
||
|
|
if (!Seen.Contains(It.Key()))
|
||
|
|
{
|
||
|
|
It.RemoveCurrent();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::FirePending()
|
||
|
|
{
|
||
|
|
if (!Pending.IsSet() || FPlatformTime::Seconds() < Pending->FireAt)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const FLPBonusPending Fire = MoveTemp(Pending.GetValue());
|
||
|
|
Pending.Reset();
|
||
|
|
UObject* Person = Fire.Person.Get();
|
||
|
|
if (!IsValid(Person))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : personne disparue avant la fin du tempo, pas de recompense"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
OnOrgasm(Person, Fire.Tags, Fire.Count);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::OnOrgasm(UObject* Person, const TArray<FString>& TagsCapturedAtDetection, int32 Count)
|
||
|
|
{
|
||
|
|
const FString Name = PersonName(Person);
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : orgasme de %s (compteur %d)"), *Name, Count);
|
||
|
|
|
||
|
|
if (GetDefault<ULPBonusRewardSettings>()->bDisabled)
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : désactivé (bDisabled)"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
UGameInstance* GI = GetGameInstance();
|
||
|
|
const bool bAttraction = GI && AttractionAllowed(GI, Person);
|
||
|
|
TArray<FString> Stats;
|
||
|
|
TArray<bool> Negative;
|
||
|
|
Pick(TagsCapturedAtDetection, Rng, bAttraction, Stats, Negative);
|
||
|
|
if (Stats.Num() == 0)
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : aucune récompense éligible pour %s"), *Name);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// V-min : la premiere recompense retenue est appliquee (l'auteur en propose jusqu'a 3 et le joueur choisit).
|
||
|
|
double X = 0.0;
|
||
|
|
if (!QueryStat(Person, Stats[0], X))
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const double Change = RewardChange(X, Negative[0]);
|
||
|
|
if (!ModifyStat(Person, Stats[0], Change))
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : %s %s %+.1f (x=%.2f, tags=%d)"),
|
||
|
|
*Name, *Stats[0], Change, X, TagsCapturedAtDetection.Num());
|
||
|
|
|
||
|
|
// ORG5-I2a : ligne affichee dans le menu d'actions (EquipMenu de Cursor) ; un echec d'affichage laisse la
|
||
|
|
// recompense appliquee (garde 1). bDisabled est sorti plus haut : rien n'est affiche (Q-I2-4).
|
||
|
|
ShowAppliedReward(Name, Stats[0], Change);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::ShowAppliedReward(const FString& Name, const FString& Stat, double Change)
|
||
|
|
{
|
||
|
|
// STE-ORG5-I2-SPEC.md option (a), section 2.2 : la ligne va dans une case ajoutee comme dernier enfant d'EquipMenu
|
||
|
|
// (emplacement de BonusRewardBox chez l'auteur, SPEC 1.1 A1 a A4) ; elle suit donc l'ouverture du menu et la
|
||
|
|
// visibilite d'EquipMenu (liee a GI.SexOn, SPEC 1.1 A7) sans code de plus. Appel depuis FirePending (ticker).
|
||
|
|
if (!bInScene)
|
||
|
|
{
|
||
|
|
// Tempo de 0,2 s fini apres la fin de la scene : pas de ligne (elle serait retiree au sondage suivant).
|
||
|
|
UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : scene terminee avant l'affichage, ligne non affichee"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
UGameInstance* GI = GetGameInstance();
|
||
|
|
if (!GI)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
UUserWidget* Cursor = FindCursor(GI);
|
||
|
|
if (!Cursor)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
UPanelWidget* Equip = FindEquipMenu(Cursor);
|
||
|
|
if (!Equip)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
EnsureEquipMenuHeight(Equip);
|
||
|
|
UVerticalBox* Box = EnsureRewardBox(Cursor, Equip);
|
||
|
|
if (!Box)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remplacement (SPEC 2.2 point 4, V6-3) : au plus MaxRewardLines lignes, la plus ancienne retiree d'abord.
|
||
|
|
while (RewardLines.Num() != 0 && RewardLines.Num() >= LPBonusReward::MaxRewardLines)
|
||
|
|
{
|
||
|
|
LPBonusReward::RemoveLine(RewardLines[0]);
|
||
|
|
RewardLines.RemoveAt(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ligne creee dans l'arbre de widgets de Cursor (UWidgetTree::ConstructWidget, WidgetTree.h:106-117 :
|
||
|
|
// NewObject avec l'arbre pour outer), nom automatique.
|
||
|
|
UTextBlock* Line = Cursor->WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass());
|
||
|
|
if (!Line)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Meme format que la ligne de journal ci-dessus (%+.1f, SPEC Q-I2-6).
|
||
|
|
Line->SetText(FText::FromString(FString::Printf(TEXT("Bonus : %s %s %+.1f"), *Name, *Stat, Change)));
|
||
|
|
Line->SetFont(LPBonusReward::RewardFont(Cursor));
|
||
|
|
Line->SetColorAndOpacity(FSlateColor(FLinearColor::White));
|
||
|
|
if (UVerticalBoxSlot* LineSlot = Box->AddChildToVerticalBox(Line))
|
||
|
|
{
|
||
|
|
// Marges de ScrollBonuses chez l'auteur (SPEC 1.1 A4) : gauche 10, haut 5, droite 10.
|
||
|
|
LineSlot->SetPadding(FMargin(10.f, 5.f, 10.f, 0.f));
|
||
|
|
LineSlot->SetHorizontalAlignment(HAlign_Left);
|
||
|
|
}
|
||
|
|
FLPBonusRewardLine Entry;
|
||
|
|
Entry.Line = Line;
|
||
|
|
Entry.ExpireAt = FPlatformTime::Seconds() + LPBonusReward::RewardLineSeconds;
|
||
|
|
RewardLines.Add(Entry);
|
||
|
|
UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : ligne affichee pour %.0f s"), LPBonusReward::RewardLineSeconds);
|
||
|
|
}
|
||
|
|
|
||
|
|
UUserWidget* ULPBonusRewardSubsystem::FindCursor(UObject* GI)
|
||
|
|
{
|
||
|
|
// Garde 1 : GI.Cursor est une propriete Blueprint, lue par reflexion comme GI.PlayerCharacter ; type reel non lu,
|
||
|
|
// verifie par Cast en UUserWidget. Introuvable : un avertissement par cause, la recompense reste appliquee.
|
||
|
|
UObject* Value = nullptr;
|
||
|
|
if (!LPBonusReward::ReadObject(GI, LPBonusReward::CursorProp, Value))
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("GI.Cursor")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de propriete objet Cursor dans %s, recompense appliquee sans affichage"),
|
||
|
|
*GI->GetClass()->GetName());
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
UUserWidget* Cursor = Cast<UUserWidget>(Value);
|
||
|
|
if (!IsValid(Cursor) || !Cursor->WidgetTree)
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("GI.Cursor.Value")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : GI.Cursor vide, sans arbre de widgets ou d'une classe autre que UserWidget (%s), recompense appliquee sans affichage"),
|
||
|
|
*GetNameSafe(Value ? Value->GetClass() : nullptr));
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return Cursor;
|
||
|
|
}
|
||
|
|
|
||
|
|
UPanelWidget* ULPBonusRewardSubsystem::FindEquipMenu(UUserWidget* Cursor)
|
||
|
|
{
|
||
|
|
// UUserWidget::GetWidgetFromName = WidgetTree->FindWidget(Name) (UserWidget.cpp:1259-1262), parcours de l'arbre
|
||
|
|
// (WidgetTree.cpp:40-54) : trouve aussi un widget qui n'est pas une variable. Type verifie par Cast (garde 1).
|
||
|
|
UWidget* Found = Cursor->GetWidgetFromName(FName(LPBonusReward::EquipMenuName));
|
||
|
|
UPanelWidget* Equip = Cast<UPanelWidget>(Found);
|
||
|
|
if (!IsValid(Equip))
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("Cursor.EquipMenu")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de panneau EquipMenu dans %s (trouve : %s), recompense appliquee sans affichage"),
|
||
|
|
*Cursor->GetClass()->GetName(), *GetNameSafe(Found ? Found->GetClass() : nullptr));
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return Equip;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::EnsureEquipMenuHeight(UPanelWidget* Equip)
|
||
|
|
{
|
||
|
|
// Q-I2-3 : hauteur de l'auteur (500, SPEC 1.1 A3) si elle est inferieure (350 chez nous, SPEC 1.4 N1). EquipMenu
|
||
|
|
// est enfant du CanvasPanel BelowCursor : sa hauteur est celle de son slot (UCanvasPanelSlot::GetSize / SetSize,
|
||
|
|
// CanvasPanelSlot.h:116, :112), qui n'a ce sens que sans taille automatique (GetAutoSize, :148) et sans ancres
|
||
|
|
// etirees verticalement (GetAnchors, :132 ; FAnchors::IsStretchedVertical, Anchors.h:78). Sinon : rien change.
|
||
|
|
UPanelSlot* EquipSlot = Equip->Slot.Get();
|
||
|
|
UCanvasPanelSlot* CanvasSlot = Cast<UCanvasPanelSlot>(EquipSlot);
|
||
|
|
if (!CanvasSlot)
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("EquipMenu.Slot")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : EquipMenu n'est pas dans un CanvasPanel (slot %s), hauteur inchangee"),
|
||
|
|
*GetNameSafe(EquipSlot ? EquipSlot->GetClass() : nullptr));
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (CanvasSlot->GetAutoSize() || CanvasSlot->GetAnchors().IsStretchedVertical())
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("EquipMenu.Slot.Layout")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : slot d'EquipMenu en taille automatique ou etire verticalement, hauteur inchangee"));
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const FVector2D Size = CanvasSlot->GetSize();
|
||
|
|
if (Size.Y < LPBonusReward::EquipMenuMinHeight)
|
||
|
|
{
|
||
|
|
CanvasSlot->SetSize(FVector2D(Size.X, LPBonusReward::EquipMenuMinHeight));
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : hauteur d'EquipMenu portee de %.0f a %.0f"), Size.Y, LPBonusReward::EquipMenuMinHeight);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
UVerticalBox* ULPBonusRewardSubsystem::EnsureRewardBox(UUserWidget* Cursor, UPanelWidget* Equip)
|
||
|
|
{
|
||
|
|
// Garde 2 (aucun ajout en double) : la case est retrouvee avant toute creation, dans l'ordre : pointeur faible
|
||
|
|
// encore enfant de cet EquipMenu ; enfant d'EquipMenu de ce nom ; objet de ce nom dans l'arbre de Cursor (case
|
||
|
|
// detachee, reprise au lieu d'un NewObject du meme nom). Creee seulement si aucune n'existe.
|
||
|
|
UVerticalBox* Box = RewardBox.Get();
|
||
|
|
if (IsValid(Box) && Box->GetParent() == Equip)
|
||
|
|
{
|
||
|
|
return Box;
|
||
|
|
}
|
||
|
|
|
||
|
|
Box = nullptr;
|
||
|
|
const FName BoxName(LPBonusReward::RewardBoxName);
|
||
|
|
bool bAttached = false;
|
||
|
|
for (int32 Index = 0; Index < Equip->GetChildrenCount(); ++Index)
|
||
|
|
{
|
||
|
|
UWidget* Child = Equip->GetChildAt(Index);
|
||
|
|
if (Child && Child->GetFName() == BoxName)
|
||
|
|
{
|
||
|
|
Box = Cast<UVerticalBox>(Child);
|
||
|
|
bAttached = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
UWidgetTree* Tree = Cursor->WidgetTree.Get();
|
||
|
|
if (!bAttached)
|
||
|
|
{
|
||
|
|
// FindObjectFast<T>(Outer, Name) : UObjectGlobals.h:2059 ; UObject pour voir un objet de ce nom de toute classe.
|
||
|
|
if (UObject* Existing = FindObjectFast<UObject>(Tree, BoxName))
|
||
|
|
{
|
||
|
|
Box = Cast<UVerticalBox>(Existing);
|
||
|
|
if (IsValid(Box))
|
||
|
|
{
|
||
|
|
Box->RemoveFromParent();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Box = Tree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), BoxName);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!IsValid(Box))
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("LPBonusRewardBox.Type")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : un objet %s existe deja sans etre une VerticalBox utilisable, recompense appliquee sans affichage"),
|
||
|
|
LPBonusReward::RewardBoxName);
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
if (!bAttached)
|
||
|
|
{
|
||
|
|
// UPanelWidget::AddChild (PanelWidget.h:59) ajoute en dernier : meme indice que BonusRewardBox chez l'auteur.
|
||
|
|
UPanelSlot* BoxSlot = Equip->AddChild(Box);
|
||
|
|
if (!BoxSlot)
|
||
|
|
{
|
||
|
|
if (FirstWarning(TEXT("EquipMenu.AddChild")))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : EquipMenu (%s) refuse un enfant de plus, recompense appliquee sans affichage"),
|
||
|
|
*Equip->GetClass()->GetName());
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
// Slot Fill comme BonusRewardBox chez l'auteur (SPEC 1.1 A4) ; autre panneau que VerticalBox : slot laisse tel quel.
|
||
|
|
if (UVerticalBoxSlot* VerticalSlot = Cast<UVerticalBoxSlot>(BoxSlot))
|
||
|
|
{
|
||
|
|
VerticalSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Case retrouvee ou reprise : ses anciennes lignes ne sont plus suivies, elles sont retirees ici.
|
||
|
|
Box->ClearChildren();
|
||
|
|
RewardLines.Reset();
|
||
|
|
RewardBox = Box;
|
||
|
|
if (!bDisplayTargetLogged)
|
||
|
|
{
|
||
|
|
bDisplayTargetLogged = true;
|
||
|
|
UE_LOG(LogLifePlay, Log, TEXT("Bonus : affichage dans EquipMenu (classe %s, %d enfants)"),
|
||
|
|
*Equip->GetClass()->GetName(), Equip->GetChildrenCount());
|
||
|
|
}
|
||
|
|
return Box;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::ExpireRewardLines()
|
||
|
|
{
|
||
|
|
// Q-I2-2 : chaque ligne est retiree 20 s apres son affichage. Echeance en temps reel (FPlatformTime::Seconds, meme
|
||
|
|
// horloge que le tempo de 0,2 s), relue a chaque passage du ticker existant (0,1 s) : retrait entre 20,0 et 20,1 s.
|
||
|
|
if (RewardLines.Num() == 0)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const double Now = FPlatformTime::Seconds();
|
||
|
|
for (int32 Index = RewardLines.Num() - 1; Index >= 0; --Index)
|
||
|
|
{
|
||
|
|
if (Now >= RewardLines[Index].ExpireAt || !RewardLines[Index].Line.IsValid())
|
||
|
|
{
|
||
|
|
LPBonusReward::RemoveLine(RewardLines[Index]);
|
||
|
|
RewardLines.RemoveAt(Index);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::ClearRewardDisplay(bool bRemoveWidgets)
|
||
|
|
{
|
||
|
|
// Garde 3 : fin de scene (Poll, SexOn faux) : lignes retirees de l'ecran et minuteries videes ; Deinitialize :
|
||
|
|
// minuteries videes seulement.
|
||
|
|
if (RewardLines.Num() == 0)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (bRemoveWidgets)
|
||
|
|
{
|
||
|
|
for (const FLPBonusRewardLine& Entry : RewardLines)
|
||
|
|
{
|
||
|
|
LPBonusReward::RemoveLine(Entry);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
RewardLines.Reset();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::AttractionAllowed(UObject* GI, UObject* Person)
|
||
|
|
{
|
||
|
|
// Condition de l'auteur (SPEC 1.1 E6c) : NON Person.isPlayer ET GI.IsInterestedIn(Person, GI.PlayerCharacter) ET
|
||
|
|
// GI.SexPersons contient GI.PlayerCharacter. Propriete ou fonction absente : avertissement, cle refusee.
|
||
|
|
const FBoolProperty* IsPlayerProp = CastField<FBoolProperty>(LPBonusReward::FindProp(Person, TEXT("isPlayer")));
|
||
|
|
if (!IsPlayerProp)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("Person.isPlayer"), FString::Printf(TEXT("pas de propriete booleenne isPlayer dans %s, cle attractiontoplayer refusee"),
|
||
|
|
*Person->GetClass()->GetName()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (IsPlayerProp->GetPropertyValue_InContainer(Person))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
UObject* Player = nullptr;
|
||
|
|
if (!LPBonusReward::ReadObject(GI, TEXT("PlayerCharacter"), Player))
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("GI.PlayerCharacter"), FString::Printf(TEXT("pas de propriete objet PlayerCharacter dans %s, cle attractiontoplayer refusee"),
|
||
|
|
*GI->GetClass()->GetName()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (!IsValid(Player))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
TArray<UObject*> Persons;
|
||
|
|
if (!LPBonusReward::ReadObjects(GI, TEXT("SexPersons"), Persons) || !Persons.Contains(Player))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
bool bInterested = false;
|
||
|
|
if (!IsInterestedIn(GI, Person, Player, bInterested))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return bInterested;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::IsInterestedIn(UObject* GI, UObject* WhoIsInterested, UObject* WhoInterests, bool& bOutInterested)
|
||
|
|
{
|
||
|
|
// GI.IsInterestedIn(WhoIsInterested, WhoInterests) -> Interested (L6f_dump_GI.txt:23421-23424, :23442-23443).
|
||
|
|
bOutInterested = false;
|
||
|
|
UFunction* Function = GI->FindFunction(FName(TEXT("IsInterestedIn")));
|
||
|
|
if (!Function)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("GI.IsInterestedIn"), FString::Printf(TEXT("fonction IsInterestedIn introuvable dans %s, cle attractiontoplayer refusee"),
|
||
|
|
*GI->GetClass()->GetName()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const FObjectPropertyBase* WhoIsParm = CastField<FObjectPropertyBase>(LPBonusReward::FindParm(Function, TEXT("WhoIsInterested")));
|
||
|
|
const FObjectPropertyBase* WhoParm = CastField<FObjectPropertyBase>(LPBonusReward::FindParm(Function, TEXT("WhoInterests")));
|
||
|
|
const FBoolProperty* ResultParm = CastField<FBoolProperty>(LPBonusReward::FindParm(Function, TEXT("Interested")));
|
||
|
|
if (!WhoIsParm || !WhoParm || !ResultParm || !ResultParm->HasAnyPropertyFlags(CPF_OutParm)
|
||
|
|
|| !WhoIsParm->PropertyClass || !WhoParm->PropertyClass)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("GI.IsInterestedIn.Parms"), FString::Printf(TEXT("signature inattendue de IsInterestedIn : %s ; cle attractiontoplayer refusee"),
|
||
|
|
*LPBonusReward::DescribeParms(Function)));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (!WhoIsInterested->IsA(WhoIsParm->PropertyClass) || !WhoInterests->IsA(WhoParm->PropertyClass))
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("GI.IsInterestedIn.Types"), FString::Printf(TEXT("IsInterestedIn attend %s et %s, recu %s et %s ; cle attractiontoplayer refusee"),
|
||
|
|
*WhoIsParm->PropertyClass->GetName(), *WhoParm->PropertyClass->GetName(),
|
||
|
|
*WhoIsInterested->GetClass()->GetName(), *WhoInterests->GetClass()->GetName()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const int32 ParmsSize = Function->ParmsSize;
|
||
|
|
const int32 Alignment = Function->GetMinAlignment();
|
||
|
|
uint8* Memory = static_cast<uint8*>(FMemory_Alloca_Aligned(ParmsSize, Alignment));
|
||
|
|
LPBonusReward::FScopedParms Parms(Function, Memory);
|
||
|
|
WhoIsParm->SetObjectPropertyValue_InContainer(Memory, WhoIsInterested);
|
||
|
|
WhoParm->SetObjectPropertyValue_InContainer(Memory, WhoInterests);
|
||
|
|
GI->ProcessEvent(Function, Memory);
|
||
|
|
bOutInterested = ResultParm->GetPropertyValue_InContainer(Memory);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::QueryStat(UObject* Person, const FString& Stat, double& OutValue)
|
||
|
|
{
|
||
|
|
// Person.QueryStat(Stat, WithoutBuff, RNG) -> Value, Bonus (L6f_dump_Person.txt:20-29) ; appel de l'auteur :
|
||
|
|
// QueryStat(Stat, false, false).Value (SPEC 1.1 E6d).
|
||
|
|
OutValue = 0.0;
|
||
|
|
UFunction* Function = Person->FindFunction(FName(TEXT("QueryStat")));
|
||
|
|
if (!Function)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("Person.QueryStat"), FString::Printf(TEXT("fonction QueryStat introuvable dans %s, recompense abandonnee"),
|
||
|
|
*Person->GetClass()->GetName()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const FStrProperty* StatParm = CastField<FStrProperty>(LPBonusReward::FindParm(Function, TEXT("Stat")));
|
||
|
|
const FProperty* ValueParm = LPBonusReward::FindParm(Function, TEXT("Value"));
|
||
|
|
const bool bSignature = StatParm && LPBonusReward::IsReal(ValueParm) && ValueParm->HasAnyPropertyFlags(CPF_OutParm)
|
||
|
|
&& CastField<FBoolProperty>(LPBonusReward::FindParm(Function, TEXT("WithoutBuff")))
|
||
|
|
&& CastField<FBoolProperty>(LPBonusReward::FindParm(Function, TEXT("RNG")));
|
||
|
|
if (!bSignature)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("Person.QueryStat.Parms"), FString::Printf(TEXT("signature inattendue de QueryStat : %s ; recompense abandonnee"),
|
||
|
|
*LPBonusReward::DescribeParms(Function)));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const int32 ParmsSize = Function->ParmsSize;
|
||
|
|
const int32 Alignment = Function->GetMinAlignment();
|
||
|
|
uint8* Memory = static_cast<uint8*>(FMemory_Alloca_Aligned(ParmsSize, Alignment));
|
||
|
|
LPBonusReward::FScopedParms Parms(Function, Memory);
|
||
|
|
StatParm->SetPropertyValue_InContainer(Memory, Stat);
|
||
|
|
LPBonusReward::SetBoolParm(Function, Memory, TEXT("WithoutBuff"), false);
|
||
|
|
LPBonusReward::SetBoolParm(Function, Memory, TEXT("RNG"), false);
|
||
|
|
Person->ProcessEvent(Function, Memory);
|
||
|
|
OutValue = LPBonusReward::ReadReal(ValueParm, Memory);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::ModifyStat(UObject* Person, const FString& Stat, double Change)
|
||
|
|
{
|
||
|
|
// Person.ModifyStat(Stat, Change, DifficultyAdjusted, NoDisplay) -> Value (L6f_dump_Person.txt:561-570) ; appel de
|
||
|
|
// l'auteur au clic : ModifyStat(Stat, Change, false, false) (SPEC 1.1 E7).
|
||
|
|
UFunction* Function = Person->FindFunction(FName(TEXT("ModifyStat")));
|
||
|
|
if (!Function)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("Person.ModifyStat"), FString::Printf(TEXT("fonction ModifyStat introuvable dans %s, recompense abandonnee"),
|
||
|
|
*Person->GetClass()->GetName()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const FStrProperty* StatParm = CastField<FStrProperty>(LPBonusReward::FindParm(Function, TEXT("Stat")));
|
||
|
|
const FProperty* ChangeParm = LPBonusReward::FindParm(Function, TEXT("Change"));
|
||
|
|
const bool bSignature = StatParm && LPBonusReward::IsReal(ChangeParm) && !ChangeParm->HasAnyPropertyFlags(CPF_OutParm)
|
||
|
|
&& CastField<FBoolProperty>(LPBonusReward::FindParm(Function, TEXT("DifficultyAdjusted")))
|
||
|
|
&& CastField<FBoolProperty>(LPBonusReward::FindParm(Function, TEXT("NoDisplay")));
|
||
|
|
if (!bSignature)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("Person.ModifyStat.Parms"), FString::Printf(TEXT("signature inattendue de ModifyStat : %s ; recompense abandonnee"),
|
||
|
|
*LPBonusReward::DescribeParms(Function)));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const int32 ParmsSize = Function->ParmsSize;
|
||
|
|
const int32 Alignment = Function->GetMinAlignment();
|
||
|
|
uint8* Memory = static_cast<uint8*>(FMemory_Alloca_Aligned(ParmsSize, Alignment));
|
||
|
|
LPBonusReward::FScopedParms Parms(Function, Memory);
|
||
|
|
StatParm->SetPropertyValue_InContainer(Memory, Stat);
|
||
|
|
LPBonusReward::WriteReal(ChangeParm, Memory, Change);
|
||
|
|
LPBonusReward::SetBoolParm(Function, Memory, TEXT("DifficultyAdjusted"), false);
|
||
|
|
LPBonusReward::SetBoolParm(Function, Memory, TEXT("NoDisplay"), false);
|
||
|
|
Person->ProcessEvent(Function, Memory);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
FString ULPBonusRewardSubsystem::PersonName(const UObject* Person)
|
||
|
|
{
|
||
|
|
// Name_First : texte chez l'auteur (SPEC 2.4) ; type non lu chez nous, texte et chaine acceptes.
|
||
|
|
const FProperty* Prop = LPBonusReward::FindProp(Person, TEXT("Name_First"));
|
||
|
|
FString Name;
|
||
|
|
if (const FTextProperty* AsText = CastField<FTextProperty>(Prop))
|
||
|
|
{
|
||
|
|
Name = AsText->GetPropertyValue_InContainer(Person).ToString();
|
||
|
|
}
|
||
|
|
else if (const FStrProperty* AsString = CastField<FStrProperty>(Prop))
|
||
|
|
{
|
||
|
|
Name = AsString->GetPropertyValue_InContainer(Person);
|
||
|
|
}
|
||
|
|
else if (Person)
|
||
|
|
{
|
||
|
|
WarnOnce(TEXT("Person.Name_First"), FString::Printf(TEXT("pas de propriete texte ou chaine Name_First dans %s, nom d'objet journalise a la place"),
|
||
|
|
*Person->GetClass()->GetName()));
|
||
|
|
}
|
||
|
|
return Name.IsEmpty() ? GetNameSafe(Person) : Name;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ULPBonusRewardSubsystem::FirstWarning(const FString& Key)
|
||
|
|
{
|
||
|
|
// D1 (STE-ORG5-V1-VAL.md) : la cause est testee avant que l'appelant construise son message.
|
||
|
|
if (Warned.Contains(Key))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
Warned.Add(Key);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ULPBonusRewardSubsystem::WarnOnce(const FString& Key, const FString& Message)
|
||
|
|
{
|
||
|
|
if (FirstWarning(Key))
|
||
|
|
{
|
||
|
|
UE_LOG(LogLifePlay, Warning, TEXT("Bonus : %s"), *Message);
|
||
|
|
}
|
||
|
|
}
|