// 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 Tags; }; /** Orgasme detecte, en attente de la fin du tempo de 0,2 s. */ struct FLPBonusPending { TWeakObjectPtr Person; TArray 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 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& 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& A, const TArray& 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& PositionTags, FRandomStream& InRng, bool bAttractionAllowed, TArray& OutStats, TArray& 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& 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, int32> LastCounts; TOptional Pending; /** Causes d'avertissement deja journalisees (un avertissement par cause). */ TSet Warned; /** Lignes affichees, une echeance par ligne ; videe en fin de scene et dans Deinitialize. */ TArray RewardLines; /** Case LPBonusRewardBox ajoutee a EquipMenu (pointeur faible : Cursor et EquipMenu peuvent etre recrees). */ TWeakObjectPtr 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; };