Added commissions system

This commit is contained in:
2026-06-01 00:27:56 +03:00
parent dd7ed121fc
commit 9a5a0003b1
81 changed files with 2418 additions and 1065 deletions
@@ -0,0 +1,28 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "BeFullyNakedNearNPCsObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.BeFullyNakedNearNPCs"
bool UBeFullyNakedNearNPCsObjective::IsConditionMet() const
{
return IsFullyNaked() && GetObserverCount() >= RequiredNPCs;
}
FText UBeFullyNakedNearNPCsObjective::GetDescription() const
{
const FText People = (RequiredNPCs == 1)
? LOCTEXT("Person", "1 person")
: FText::Format(LOCTEXT("People", "{0} people"), FText::AsNumber(RequiredNPCs));
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Be fully naked in front of {0} for {1} seconds"),
People, FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return FText::Format(LOCTEXT("Instant", "Get fully naked in front of {0}"), People);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,25 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "BeFullyNakedNearNPCsObjective.generated.h"
// §13.4 BeFullyNakedNearNPCs(count, durationSeconds): fully naked while at least `count` NPCs observe,
// sustained for the duration. Observer count is inherited from UObserverObjectiveBase.
UCLASS(EditInlineNew, DisplayName = "Be Fully Naked Near NPCs")
class NAKEDDESIRE_API UBeFullyNakedNearNPCsObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override;
private:
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
int32 RequiredNPCs = 1;
};
@@ -0,0 +1,19 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "BeFullyNakedObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.BeFullyNaked"
FText UBeFullyNakedObjective::GetDescription() const
{
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Be fully naked for {0} seconds"),
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return LOCTEXT("Instant", "Get fully naked");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "BeFullyNakedObjective.generated.h"
// §13.4 BeFullyNaked(durationSeconds): fully unclothed for RequiredHoldSeconds; no NPC requirement.
UCLASS(EditInlineNew, DisplayName = "Be Fully Naked")
class NAKEDDESIRE_API UBeFullyNakedObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override { return IsFullyNaked(); }
};
@@ -0,0 +1,23 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "BeObservedWhileExposedObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.BeObservedWhileExposed"
FText UBeObservedWhileExposedObjective::GetDescription() const
{
const FText People = (RequiredObservers == 1)
? LOCTEXT("Person", "someone")
: FText::Format(LOCTEXT("People", "{0} people"), FText::AsNumber(RequiredObservers));
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Stay exposed in front of {0} for {1} seconds"),
People, FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return FText::Format(LOCTEXT("Instant", "Get exposed in front of {0}"), People);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,25 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "BeObservedWhileExposedObjective.generated.h"
// At least RequiredObservers NPCs observing while any region is revealing (not necessarily fully nude).
// Generalizes "naked near NPCs" to partial exposure.
UCLASS(EditInlineNew, DisplayName = "Be Observed While Exposed")
class NAKEDDESIRE_API UBeObservedWhileExposedObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override { return IsAnyPartRevealing() && GetObserverCount() >= RequiredObservers; }
private:
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
int32 RequiredObservers = 1;
};
@@ -0,0 +1,60 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "CoverageObjectiveBase.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
void UCoverageObjectiveBase::OnActivate()
{
Super::OnActivate(); // observer subscription
if (Player && Player->ClothingManager)
{
Player->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
Player->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
}
}
void UCoverageObjectiveBase::OnDeactivate()
{
if (Player && Player->ClothingManager)
{
Player->ClothingManager->OnClothingEquip.RemoveDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
Player->ClothingManager->OnClothingUnequip.RemoveDynamic(this, &UCoverageObjectiveBase::HandleClothingChanged);
}
Super::OnDeactivate(); // observer unsubscription
}
bool UCoverageObjectiveBase::IsFullyNaked() const
{
if (!Player || !Player->ClothingManager)
return false;
UClothingManager* CM = Player->ClothingManager;
return CM->GetEffectiveCoverage(EBodyPart::Boobs) <= 0.0f
&& CM->GetEffectiveCoverage(EBodyPart::Ass) <= 0.0f
&& CM->GetEffectiveCoverage(EBodyPart::Genitals) <= 0.0f;
}
bool UCoverageObjectiveBase::IsPartRevealing(EBodyPart Part) const
{
if (!Player || !Player->ClothingManager)
return false;
return Player->ClothingManager->GetEffectiveCoverage(Part) < Player->ObservationRevealThreshold;
}
bool UCoverageObjectiveBase::IsAnyPartRevealing() const
{
return IsPartRevealing(EBodyPart::Boobs)
|| IsPartRevealing(EBodyPart::Ass)
|| IsPartRevealing(EBodyPart::Genitals);
}
void UCoverageObjectiveBase::HandleClothingChanged(UClothingItemInstance* ClothingItemInstance)
{
NotifyConditionChanged();
}
@@ -0,0 +1,36 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Clothing/BodyPart.h"
#include "ObserverObjectiveBase.h"
#include "CoverageObjectiveBase.generated.h"
class UClothingItemInstance;
// Shared base for objectives that read body coverage; re-evaluates on any equip / unequip. Inherits
// the observer subscription from UObserverObjectiveBase, so coverage objectives can also gate on the
// current observer count (e.g. "naked in front of N people") without extra wiring.
UCLASS(Abstract)
class NAKEDDESIRE_API UCoverageObjectiveBase : public UObserverObjectiveBase
{
GENERATED_BODY()
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
// Fully unclothed: no effective coverage on any of the three exposable regions.
bool IsFullyNaked() const;
// Part reads as "revealing" — below the player's observation reveal threshold.
bool IsPartRevealing(EBodyPart Part) const;
// Any of the three regions is revealing.
bool IsAnyPartRevealing() const;
private:
UFUNCTION()
void HandleClothingChanged(UClothingItemInstance* ClothingItemInstance);
};
@@ -0,0 +1,33 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "ExposeBodyPartObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.ExposeBodyPart"
namespace
{
FText BodyPartText(EBodyPart Part)
{
switch (Part)
{
case EBodyPart::Boobs: return LOCTEXT("Boobs", "boobs");
case EBodyPart::Ass: return LOCTEXT("Ass", "ass");
case EBodyPart::Genitals: return LOCTEXT("Genitals", "genitals");
default: return LOCTEXT("None", "nothing");
}
}
}
FText UExposeBodyPartObjective::GetDescription() const
{
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Expose your {0} for {1} seconds"),
BodyPartText(Part), FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return FText::Format(LOCTEXT("Instant", "Expose your {0}"), BodyPartText(Part));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,25 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Clothing/BodyPart.h"
#include "CoverageObjectiveBase.h"
#include "ExposeBodyPartObjective.generated.h"
// §13.4 ExposeBodyPart(part, durationSeconds): the named part reads as revealing for the duration.
UCLASS(EditInlineNew, DisplayName = "Expose Body Part")
class NAKEDDESIRE_API UExposeBodyPartObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override { return IsPartRevealing(Part); }
private:
UPROPERTY(EditDefaultsOnly)
EBodyPart Part = EBodyPart::Boobs;
};
@@ -0,0 +1,19 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "GatherCrowdObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.GatherCrowd"
FText UGatherCrowdObjective::GetDescription() const
{
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Hold a crowd of {0} watchers for {1} seconds"),
FText::AsNumber(RequiredObservers), FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return FText::Format(LOCTEXT("Instant", "Gather a crowd of {0} watchers"), FText::AsNumber(RequiredObservers));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,25 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "ObserverObjectiveBase.h"
#include "GatherCrowdObjective.generated.h"
// Draw a crowd: at least RequiredObservers NPCs observing the player simultaneously. With
// RequiredHoldSeconds > 0 the crowd must hold for that long; 0 = the instant the count is reached.
UCLASS(EditInlineNew, DisplayName = "Gather a Crowd")
class NAKEDDESIRE_API UGatherCrowdObjective : public UObserverObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override { return GetObserverCount() >= RequiredObservers; }
private:
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
int32 RequiredObservers = 3;
};
@@ -0,0 +1,29 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "ObserverObjectiveBase.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/Stats/StatsManager.h"
void UObserverObjectiveBase::OnActivate()
{
if (Player && Player->StatsManager)
Player->StatsManager->OnObserversChanged.AddUniqueDynamic(this, &UObserverObjectiveBase::HandleObserversChanged);
}
void UObserverObjectiveBase::OnDeactivate()
{
if (Player && Player->StatsManager)
Player->StatsManager->OnObserversChanged.RemoveDynamic(this, &UObserverObjectiveBase::HandleObserversChanged);
}
int32 UObserverObjectiveBase::GetObserverCount() const
{
return (Player && Player->StatsManager) ? Player->StatsManager->GetObserverCount() : 0;
}
void UObserverObjectiveBase::HandleObserversChanged()
{
NotifyConditionChanged();
}
@@ -0,0 +1,25 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Commissions/CommissionObjective.h"
#include "ObserverObjectiveBase.generated.h"
// Base for objectives that react to how many NPCs are currently observing the player (the same
// observer set that drives embarrassment). Re-evaluates whenever that count changes.
UCLASS(Abstract)
class NAKEDDESIRE_API UObserverObjectiveBase : public UCommissionObjective
{
GENERATED_BODY()
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
int32 GetObserverCount() const;
private:
UFUNCTION()
void HandleObserversChanged();
};
@@ -0,0 +1,48 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "ReachEmbarrassmentObjective.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/Stats/StatsManager.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.ReachEmbarrassment"
void UReachEmbarrassmentObjective::OnActivate()
{
if (Player && Player->StatsManager)
Player->StatsManager->EmbarrassmentUpdate.AddDynamic(this, &UReachEmbarrassmentObjective::HandleEmbarrassmentUpdate);
}
void UReachEmbarrassmentObjective::OnDeactivate()
{
if (Player && Player->StatsManager)
Player->StatsManager->EmbarrassmentUpdate.RemoveDynamic(this, &UReachEmbarrassmentObjective::HandleEmbarrassmentUpdate);
}
bool UReachEmbarrassmentObjective::IsConditionMet() const
{
return CachedMax > 0.0f && CachedCurrent >= ThresholdFraction * CachedMax;
}
void UReachEmbarrassmentObjective::HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue)
{
CachedCurrent = CurrentValue;
CachedMax = MaxValue;
NotifyConditionChanged();
}
FText UReachEmbarrassmentObjective::GetDescription() const
{
const FText Percent = FText::AsNumber(FMath::RoundToInt(ThresholdFraction * 100.0f));
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Keep embarrassment above {0}% for {1} seconds"),
Percent, FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return FText::Format(LOCTEXT("Instant", "Push your embarrassment past {0}%"), Percent);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,33 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Commissions/CommissionObjective.h"
#include "ReachEmbarrassmentObjective.generated.h"
// Push embarrassment to ThresholdFraction of max. RequiredHoldSeconds = 0 satisfies on reaching it
// (ReachEmbarrassment); > 0 requires staying at/above it for the duration (SustainEmbarrassment).
UCLASS(EditInlineNew, DisplayName = "Reach Embarrassment")
class NAKEDDESIRE_API UReachEmbarrassmentObjective : public UCommissionObjective
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
virtual bool IsConditionMet() const override;
private:
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0.0, ClampMax = 1.0))
float ThresholdFraction = 0.8f;
UFUNCTION()
void HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue);
float CachedCurrent = 0.0f;
float CachedMax = 0.0f;
};
@@ -0,0 +1,72 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "RunNakedDistanceObjective.h"
#include "TimerManager.h"
#include "NakedDesire/Global/Gait.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.RunNakedDistance"
namespace
{
constexpr float SampleIntervalSeconds = 0.2f;
constexpr float MaxStepCm = 1000.0f; // ignore single-sample jumps larger than this (teleports / streaming)
}
void URunNakedDistanceObjective::OnActivate()
{
Super::OnActivate();
AccumulatedCm = 0.0f;
if (Player)
{
LastLocation = Player->GetActorLocation();
Player->GetWorldTimerManager().SetTimer(SampleTimer, this, &URunNakedDistanceObjective::SampleDistance, SampleIntervalSeconds, true);
}
}
void URunNakedDistanceObjective::OnDeactivate()
{
if (Player)
Player->GetWorldTimerManager().ClearTimer(SampleTimer);
SampleTimer.Invalidate();
Super::OnDeactivate();
}
void URunNakedDistanceObjective::SampleDistance()
{
if (bSatisfied || !Player)
return;
const FVector Now = Player->GetActorLocation();
const float Delta = FVector::Dist(Now, LastLocation);
LastLocation = Now;
// Only running, naked, and within any constraints counts toward the distance.
if (IsFullyNaked() && Player->GetGait() == EGait::Run && AreConstraintsMet())
{
AccumulatedCm += FMath::Min(Delta, MaxStepCm);
if (AccumulatedCm >= RequiredMeters * 100.0f)
MarkSatisfied();
}
}
float URunNakedDistanceObjective::GetProgress() const
{
if (bSatisfied)
return 1.0f;
const float TargetCm = RequiredMeters * 100.0f;
return TargetCm > 0.0f ? FMath::Clamp(AccumulatedCm / TargetCm, 0.0f, 1.0f) : 0.0f;
}
FText URunNakedDistanceObjective::GetDescription() const
{
return FText::Format(LOCTEXT("Run", "Run {0} m naked"), FText::AsNumber(FMath::RoundToInt(RequiredMeters)));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,33 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "RunNakedDistanceObjective.generated.h"
// Cover RequiredMeters on foot while fully naked AND in the run gait. Distance accrues only while that
// condition (and any constraints) hold; per-sample movement is clamped so a teleport can't satisfy it.
UCLASS(EditInlineNew, DisplayName = "Run Naked Distance")
class NAKEDDESIRE_API URunNakedDistanceObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
virtual float GetProgress() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
private:
void SampleDistance();
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
float RequiredMeters = 50.0f;
float AccumulatedCm = 0.0f;
FVector LastLocation = FVector::ZeroVector;
FTimerHandle SampleTimer;
};
@@ -0,0 +1,19 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "StayUnseenWhileNakedObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.StayUnseenWhileNaked"
FText UStayUnseenWhileNakedObjective::GetDescription() const
{
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Stay fully naked and unseen for {0} seconds"),
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return LOCTEXT("Instant", "Be fully naked with nobody watching");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "StayUnseenWhileNakedObjective.generated.h"
// Stealth exhibitionism: be fully naked with ZERO observers for the duration (streak between patrols).
UCLASS(EditInlineNew, DisplayName = "Stay Unseen While Naked")
class NAKEDDESIRE_API UStayUnseenWhileNakedObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override { return IsFullyNaked() && GetObserverCount() == 0; }
};
@@ -0,0 +1,42 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "WearOnlyUnderwearObjective.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.WearOnlyUnderwear"
bool UWearOnlyUnderwearObjective::IsConditionMet() const
{
if (!Player || !Player->ClothingManager)
return false;
UClothingManager* CM = Player->ClothingManager;
const bool bUnderwearWorn =
CM->IsClothingTypeOn(EClothingSlotType::UnderwearTop) ||
CM->IsClothingTypeOn(EClothingSlotType::UnderwearBottom);
const bool bOuterClear =
!CM->IsClothingTypeOn(EClothingSlotType::Outerwear) &&
!CM->IsClothingTypeOn(EClothingSlotType::Top) &&
!CM->IsClothingTypeOn(EClothingSlotType::Bottom) &&
!CM->IsClothingTypeOn(EClothingSlotType::Bodysuit);
return bUnderwearWorn && bOuterClear;
}
FText UWearOnlyUnderwearObjective::GetDescription() const
{
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Stay in just your underwear for {0} seconds"),
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return LOCTEXT("Instant", "Strip down to just your underwear");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,21 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "WearOnlyUnderwearObjective.generated.h"
// Stripped to underwear: at least one underwear slot worn while every outer body-clothing slot
// (Outerwear / Top / Bottom / Bodysuit) is empty. Socks / footwear / accessories are ignored.
UCLASS(EditInlineNew, DisplayName = "Wear Only Underwear")
class NAKEDDESIRE_API UWearOnlyUnderwearObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override;
};