LifePlay-Revival/Source/LifePlay/LPBonusRewardSubsystem.h
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

165 lines
8.9 KiB
C++

// Lot STE, fiche ORG5-I1 : bonus d'orgasme par tirage de recompenses de stats, version minimale (V-min).
//
// Origine : comportement reconstruit d'apres LifePlay 2023.04 de Vinfamy (build 2023.04 Stable, lu par
// desassemblage du bytecode de Person.GiveOrgasmReward, Cursor.PopulateBonusRewards et BonusReward ; detail et
// sources : Docs/Migration/etat/ST/STE/STE-ORG5-SPEC.md, sections 1.1 a 1.3). Le code est ecrit a neuf ; la table
// des 20 cles, le tirage et la formule reprennent le comportement lu.
// Licence : MIT (SPDX-License-Identifier: MIT). Le travail de Vinfamy est sous licence MIT
// (Licenses/Vinfamy_License.txt, ligne 1) : utilisation, copie, modification, fusion, publication, distribution,
// sous-licence et vente permises, sans aucune garantie, a condition de conserver la mention de droit d'auteur et de
// licence. Texte de reference : Licenses/Vinfamy_License.txt.
//
// Ce que fait la V-min (STE-ORG5-SPEC.md, section 3.1) :
// - Accroche H1, sans noeud Blueprint : toutes les 0,1 s, lecture par reflexion de GI.SexOn, GI.SexPersons et, pour
// chaque personne de la scene, Person.Sex_OrgasmCounts. Une hausse du compteur = un orgasme (le compteur monte
// dans l'evenement Cum de Person) ; premier passage = memorisation sans recompense ; baisse = resynchronisation.
// Aucune lecture du journal, aucun crochet de texte. Fin de scene (SexOn faux) : la memoire des compteurs est
// videe.
// - Tempo de 0,2 s comme le RetriggerableDelay de l'auteur : un second orgasme dans les 0,2 s remplace le premier,
// seul le dernier recoit des recompenses. Les tags de position (GI.CurrentTags) sont copies a la detection.
// - Tirage exact de l'auteur : 10 cles au hasard parmi les 20, filtre Overlaps(tags de position, tags de la cle,
// vide = vrai), cle attractiontoplayer reservee aux PNJ interesses par le joueur present dans la scene, 3 au plus.
// - Application automatique de la PREMIERE recompense retenue : Person.ModifyStat(Stat, Change, false, false), la
// valeur venant de Person.QueryStat(Stat, false, false). Pas de grille ni de choix (version V-1, plus tard).
// - Le bonus d'orgasme du jeu (GiveOrgasmReward : OrgasmIrBoost, mood, attractiontoplayer) n'est pas touche ; la
// recompense s'y ajoute, comme chez l'auteur.
// - Reglage : ULPBonusRewardSettings::bDisabled, section [/Script/LifePlay.LPBonusRewardSettings] de la config Game
// du projet (Config/DefaultGame.ini) ; section absente : valeur du code (faux, fonction active). Rien n'est ecrit
// par ce code (pas de SaveConfig).
// - Journal LogLifePlay, prefixe "Bonus :" (formats de la SPEC, section 2.4) ; propriete ou fonction Blueprint
// absente ou d'un type inattendu : un avertissement par cause, abandon sans effet, jamais de plantage.
// Aucun asset n'est modifie. Regle commune ORG4 / ORG5 : un orgasme (un increment de Sex_OrgasmCounts) donne au plus
// une recompense ORG5 a la personne ; Endsex sans Cum n'incremente pas le compteur et ne declenche rien ici.
//
// Fiche ORG5-I2a (STE-ORG5-I2-SPEC.md, option a, et decisions Q-I2-1 a Q-I2-4) : affichage de la recompense appliquee
// au meme emplacement que chez l'auteur, dans le menu d'actions. A l'execution, une case "LPBonusRewardBox"
// (UVerticalBox) est ajoutee comme dernier enfant du panneau EquipMenu du widget GI.Cursor ; elle recoit une ligne
// "Bonus : prenom stat +-valeur" (blanc, police 16, contour noir 2 alpha 0,8, marge gauche 10) par recompense
// appliquee, remplacee a la recompense suivante et retiree 20 s apres son affichage ou a la fin de la scene. EquipMenu
// est porte a 500 de haut s'il est plus petit. bDisabled coupe tout (application et affichage). Cursor ou EquipMenu
// introuvable : un avertissement par cause, la recompense reste appliquee. Aucun asset modifie.
#pragma once
#include "CoreMinimal.h"
#include "Containers/Ticker.h"
#include "Math/RandomStream.h"
#include "Misc/Optional.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "UObject/Object.h"
#include "LPBonusRewardSubsystem.generated.h"
class UPanelWidget;
class UTextBlock;
class UUserWidget;
class UVerticalBox;
/** Reglage du bonus de recompenses, lu dans la config Game du projet (objet par defaut seulement, jamais ecrit). */
UCLASS(config = Game)
class LIFEPLAY_API ULPBonusRewardSettings : public UObject
{
GENERATED_BODY()
public:
/** Vrai : aucune recompense tiree (equivalent de GI.DisableBonus de l'auteur) ; le bonus d'orgasme du jeu reste. */
UPROPERTY(config)
bool bDisabled = false;
};
/** Une cle de la table de l'auteur : stat (suivie de '-' si la recompense baisse la stat) et tags de position. */
struct FLPBonusStat
{
FString Key;
TArray<FString> Tags;
};
/** Orgasme detecte, en attente de la fin du tempo de 0,2 s. */
struct FLPBonusPending
{
TWeakObjectPtr<UObject> Person;
TArray<FString> Tags;
int32 Count = 0;
double FireAt = 0.0;
};
/** Ligne de recompense affichee dans EquipMenu et son echeance (minuterie de 20 s propre a la ligne). */
struct FLPBonusRewardLine
{
TWeakObjectPtr<UTextBlock> Line;
double ExpireAt = 0.0;
};
UCLASS()
class LIFEPLAY_API ULPBonusRewardSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
/** Les 20 cles de l'auteur, dans l'ordre de STE-ORG5-SPEC.md section 1.2. */
static const TArray<FLPBonusStat>& Table();
/** Formule de l'auteur : signe * ceil((2 - MapRangeClamped(|X|, 0, 100, 0, 1.9)) * 10) / 10, en double. */
static double RewardChange(double X, bool bNegative);
/** Equivalent de GI.Overlaps : vrai si un element de A est dans B ; si bIfEmptyThenTrue et A ou B vide : vrai. */
static bool Overlaps(const TArray<FString>& A, const TArray<FString>& B, bool bIfEmptyThenTrue);
/**
* Tirage de l'auteur : 10 cles melangees par Rng, filtre de tags, regle attractiontoplayer, 3 au plus.
* OutStats : noms de stat sans le '-' final ; OutNegative : vrai si la cle finissait par '-'.
*/
static void Pick(const TArray<FString>& PositionTags, FRandomStream& InRng, bool bAttractionAllowed,
TArray<FString>& OutStats, TArray<bool>& OutNegative);
/** Controle de la formule sur 0, 10, 25, 50, 75, 90, 100 et le signe ; faux : OutFailure decrit l'ecart. */
static bool CheckFormula(FString& OutFailure);
private:
bool Tick(float DeltaTime);
void Poll(UObject* GI);
void FirePending();
void OnOrgasm(UObject* Person, const TArray<FString>& TagsCapturedAtDetection, int32 Count);
bool AttractionAllowed(UObject* GI, UObject* Person);
bool IsInterestedIn(UObject* GI, UObject* WhoIsInterested, UObject* WhoInterests, bool& bOutInterested);
bool QueryStat(UObject* Person, const FString& Stat, double& OutValue);
bool ModifyStat(UObject* Person, const FString& Stat, double Change);
FString PersonName(const UObject* Person);
void WarnOnce(const FString& Key, const FString& Message);
/** Vrai la premiere fois pour Key (la cause est alors notee) : l'appelant construit son message seulement si vrai. */
bool FirstWarning(const FString& Key);
// Affichage (fiche ORG5-I2a).
/** Appele par OnOrgasm apres un ModifyStat reussi (donc depuis FirePending, jamais dans la pile de Cum). */
void ShowAppliedReward(const FString& Name, const FString& Stat, double Change);
/** GI.Cursor par reflexion, en UUserWidget avec un arbre de widgets ; nul (avertissement unique) sinon. */
UUserWidget* FindCursor(UObject* GI);
/** Cursor.GetWidgetFromName("EquipMenu"), verifie par Cast en UPanelWidget ; nul (avertissement unique) sinon. */
UPanelWidget* FindEquipMenu(UUserWidget* Cursor);
/** Hauteur d'EquipMenu portee a 500 si elle est inferieure (slot de CanvasPanel seulement). */
void EnsureEquipMenuHeight(UPanelWidget* Equip);
/** Case LPBonusRewardBox retrouvee par son nom, ou creee une seule fois comme dernier enfant d'EquipMenu. */
UVerticalBox* EnsureRewardBox(UUserWidget* Cursor, UPanelWidget* Equip);
/** Retire les lignes dont l'echeance est passee (appele a chaque passage du ticker). */
void ExpireRewardLines();
/** Vide les minuteries des lignes ; bRemoveWidgets : retire aussi les lignes de l'ecran (fin de scene). */
void ClearRewardDisplay(bool bRemoveWidgets);
FTSTicker::FDelegateHandle TickHandle;
FRandomStream Rng;
/** Dernier Sex_OrgasmCounts vu par personne de la scene ; vide hors scene. */
TMap<TWeakObjectPtr<UObject>, int32> LastCounts;
TOptional<FLPBonusPending> Pending;
/** Causes d'avertissement deja journalisees (un avertissement par cause). */
TSet<FString> Warned;
/** Lignes affichees, une echeance par ligne ; videe en fin de scene et dans Deinitialize. */
TArray<FLPBonusRewardLine> RewardLines;
/** Case LPBonusRewardBox ajoutee a EquipMenu (pointeur faible : Cursor et EquipMenu peuvent etre recrees). */
TWeakObjectPtr<UVerticalBox> RewardBox;
/** GI.SexOn au dernier sondage (faux : aucune ligne n'est affichee). */
bool bInScene = false;
/** Ligne de journal "affichage dans EquipMenu" deja ecrite (une fois par session). */
bool bDisplayTargetLogged = false;
};