Added commissions system

This commit is contained in:
2026-06-01 00:27:56 +03:00
parent a3e23393dc
commit 003d9992e2
81 changed files with 2418 additions and 1065 deletions
@@ -0,0 +1,158 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "Commission.h"
#include "CommissionObjective.h"
void UCommission::Accept(ANakedDesireCharacter* InPlayer)
{
if (State != ECommissionState::Offered)
return;
Player = InPlayer;
SetState(ECommissionState::Accepted);
ArmObjectives();
// Arming evaluates immediately, so a commission already satisfied on accept completes now.
if (AreAllObjectivesSatisfied())
Complete();
}
void UCommission::Abandon()
{
if (State != ECommissionState::Accepted)
return;
DisarmObjectives();
SetState(ECommissionState::Offered);
}
void UCommission::Expire()
{
if (State != ECommissionState::Accepted)
return;
DisarmObjectives();
SetState(ECommissionState::Expired);
}
void UCommission::RestoreState(ECommissionState SavedState, ANakedDesireCharacter* InPlayer)
{
Player = InPlayer;
if (SavedState == ECommissionState::Accepted)
{
SetState(ECommissionState::Accepted);
ArmObjectives();
if (AreAllObjectivesSatisfied())
Complete();
}
else
{
SetState(SavedState);
}
}
void UCommission::Complete()
{
if (State != ECommissionState::Accepted)
return; // already resolved — guards against double completion / double payout
DisarmObjectives();
SetState(ECommissionState::Completed);
OnCompleted.Broadcast(this);
}
bool UCommission::AreAllObjectivesSatisfied() const
{
if (Objectives.Num() == 0)
return false; // a commission with no objectives never auto-completes
for (const UCommissionObjective* Objective : Objectives)
{
if (!Objective || !Objective->IsSatisfied())
return false;
}
return true;
}
void UCommission::ArmObjectives()
{
// Subscribe to all objectives up front; activation differs by mode.
for (UCommissionObjective* Objective : Objectives)
{
if (Objective)
Objective->OnStateChanged.AddUObject(this, &UCommission::HandleObjectiveStateChanged);
}
if (bSequentialObjectives)
{
ActiveObjectiveIndex = INDEX_NONE;
AdvanceSequential();
}
else
{
for (UCommissionObjective* Objective : Objectives)
{
if (Objective)
Objective->Activate(Player);
}
}
}
void UCommission::DisarmObjectives()
{
for (UCommissionObjective* Objective : Objectives)
{
if (!Objective)
continue;
Objective->OnStateChanged.RemoveAll(this);
Objective->Deactivate();
}
ActiveObjectiveIndex = INDEX_NONE;
}
void UCommission::AdvanceSequential()
{
for (int32 Index = 0; Index < Objectives.Num(); ++Index)
{
UCommissionObjective* Objective = Objectives[Index];
if (!Objective)
continue;
if (!Objective->IsSatisfied())
{
// Activate the first unsatisfied step (only if it isn't already the armed one — Activate
// resets progress, so re-activating the live objective would wipe its hold timer).
if (ActiveObjectiveIndex != Index)
{
ActiveObjectiveIndex = Index;
Objective->Activate(Player);
}
return;
}
}
ActiveObjectiveIndex = INDEX_NONE;
Complete();
}
void UCommission::SetState(ECommissionState NewState)
{
State = NewState;
OnStateChanged.Broadcast(this);
}
void UCommission::HandleObjectiveStateChanged(UCommissionObjective* Objective)
{
if (State != ECommissionState::Accepted)
return;
if (bSequentialObjectives)
AdvanceSequential(); // the armed step finished -> activate the next, or complete
else if (AreAllObjectivesSatisfied())
Complete();
}
@@ -0,0 +1,87 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "CommissionTypes.h"
#include "Commission.generated.h"
class UCommissionObjective;
class ANakedDesireCharacter;
class UCommission;
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCommissionChangedSignature, UCommission*);
/**
* A commission (GDD §13). Authored inline in a UCommissionBoardConfig and duplicated to a runtime
* instance by UMissionSubsystem; the duplicate owns the state machine and its live objectives.
* Completes when every objective reports satisfied; the subsystem pays the reward and persists state.
*/
UCLASS(EditInlineNew, BlueprintType)
class NAKEDDESIRE_API UCommission : public UObject
{
GENERATED_BODY()
public:
// --- Authoring ---
// Stable id used for save/restore (must be unique within the board config).
UPROPERTY(EditDefaultsOnly)
FName CommissionId;
UPROPERTY(EditDefaultsOnly)
FText Title;
// Lore-only forum poster (§13.3) — no gameplay effect.
UPROPERTY(EditDefaultsOnly)
FText PosterUsername;
UPROPERTY(EditDefaultsOnly)
ECommissionTier Tier = ECommissionTier::Daily;
UPROPERTY(EditDefaultsOnly)
FCommissionReward Reward;
UPROPERTY(EditDefaultsOnly, Instanced)
TArray<UCommissionObjective*> Objectives;
// When true, objectives activate one at a time in array order (strip at A -> walk to B -> ... ).
// When false (default), all objectives are active at once and may be satisfied in any order.
UPROPERTY(EditDefaultsOnly)
bool bSequentialObjectives = false;
// --- Runtime ---
FOnCommissionChangedSignature OnStateChanged; // any transition
FOnCommissionChangedSignature OnCompleted; // -> Completed
ECommissionState GetState() const { return State; }
UFUNCTION(BlueprintPure) ECommissionTier GetTier() const { return Tier; }
UFUNCTION(BlueprintPure) FText GetTitle() const { return Title; }
UFUNCTION(BlueprintPure) FText GetPosterUsername() const { return PosterUsername; }
UFUNCTION(BlueprintPure) FCommissionReward GetReward() const { return Reward; }
UFUNCTION(BlueprintPure) TArray<UCommissionObjective*> GetObjectives() const { return Objectives; }
void Accept(ANakedDesireCharacter* InPlayer); // Offered -> Accepted; arms objectives
void Abandon(); // Accepted -> Offered; no penalty (§13.2)
void Expire(); // Accepted -> Expired (deadline)
// Re-apply a persisted state on load (re-arms objectives when restoring Accepted).
void RestoreState(ECommissionState SavedState, ANakedDesireCharacter* InPlayer);
private:
void Complete();
bool AreAllObjectivesSatisfied() const;
void ArmObjectives();
void DisarmObjectives();
void AdvanceSequential(); // activate the first unsatisfied objective, or complete if none remain
void SetState(ECommissionState NewState);
void HandleObjectiveStateChanged(UCommissionObjective* Objective);
ECommissionState State = ECommissionState::Offered;
int32 ActiveObjectiveIndex = INDEX_NONE; // sequential mode: the objective currently armed
UPROPERTY()
ANakedDesireCharacter* Player = nullptr;
};
@@ -0,0 +1,21 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CommissionBoardConfig.generated.h"
class UCommission;
// Hand-authored commission pool (GDD §13.4 procedural generation is Phase 7). For the slice the board
// simply offers everything authored here; the daily/weekly split lives on each commission's Tier.
UCLASS(BlueprintType)
class NAKEDDESIRE_API UCommissionBoardConfig : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Instanced)
TArray<UCommission*> Commissions;
};
@@ -0,0 +1,21 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "CommissionConstraint.h"
void UCommissionConstraint::Activate(ANakedDesireCharacter* InPlayer)
{
Player = InPlayer;
OnActivate();
}
void UCommissionConstraint::Deactivate()
{
OnDeactivate();
Player = nullptr;
}
FText UCommissionConstraint::GetDescription() const
{
return FText::GetEmpty();
}
@@ -0,0 +1,44 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "CommissionConstraint.generated.h"
class ANakedDesireCharacter;
DECLARE_MULTICAST_DELEGATE(FOnConstraintChangedSignature);
/**
* A "while Y" gate attached to a UCommissionObjective. The objective only counts progress while every
* constraint reports IsMet(). A constraint subscribes to whatever world signal it watches and fires
* OnConstraintChanged so the owning objective re-evaluates. Constraints compose with any objective, so
* "expose boobs" + "while at the beach" + "during night" is data, not new objective code (see §13.4).
*/
UCLASS(Abstract, EditInlineNew, BlueprintType)
class NAKEDDESIRE_API UCommissionConstraint : public UObject
{
GENERATED_BODY()
public:
FOnConstraintChangedSignature OnConstraintChanged;
void Activate(ANakedDesireCharacter* InPlayer);
void Deactivate();
virtual bool IsMet() const { return true; }
UFUNCTION(BlueprintPure)
virtual FText GetDescription() const;
protected:
UPROPERTY()
ANakedDesireCharacter* Player = nullptr;
virtual void OnActivate() {}
virtual void OnDeactivate() {}
// Subclasses call this when the thing they watch changes; the owning objective re-evaluates.
void NotifyChanged() { OnConstraintChanged.Broadcast(); }
};
@@ -0,0 +1,125 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "CommissionObjective.h"
#include "TimerManager.h"
#include "CommissionConstraint.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
void UCommissionObjective::Activate(ANakedDesireCharacter* InPlayer)
{
Player = InPlayer;
bSatisfied = false;
OnActivate();
for (UCommissionConstraint* Constraint : Constraints)
{
if (!Constraint)
continue;
Constraint->OnConstraintChanged.AddUObject(this, &UCommissionObjective::HandleConstraintChanged);
Constraint->Activate(Player);
}
NotifyConditionChanged(); // evaluate the starting state (e.g. already naked on accept)
}
void UCommissionObjective::Deactivate()
{
ClearHoldTimer();
for (UCommissionConstraint* Constraint : Constraints)
{
if (!Constraint)
continue;
Constraint->OnConstraintChanged.RemoveAll(this);
Constraint->Deactivate();
}
OnDeactivate();
Player = nullptr;
}
bool UCommissionObjective::AreConstraintsMet() const
{
for (const UCommissionConstraint* Constraint : Constraints)
{
if (Constraint && !Constraint->IsMet())
return false;
}
return true;
}
void UCommissionObjective::HandleConstraintChanged()
{
NotifyConditionChanged();
}
void UCommissionObjective::NotifyConditionChanged()
{
if (bSatisfied || !Player)
return;
if (IsConditionMet() && AreConstraintsMet())
{
if (RequiredHoldSeconds <= 0.0f)
{
MarkSatisfied();
return;
}
if (!HoldTimerHandle.IsValid())
{
HoldStartTime = Player->GetWorld()->GetTimeSeconds();
Player->GetWorldTimerManager().SetTimer(HoldTimerHandle, this, &UCommissionObjective::OnHoldElapsed, RequiredHoldSeconds, false);
}
}
else
{
ClearHoldTimer();
}
}
void UCommissionObjective::OnHoldElapsed()
{
MarkSatisfied();
}
void UCommissionObjective::MarkSatisfied()
{
if (bSatisfied)
return;
bSatisfied = true;
ClearHoldTimer();
OnStateChanged.Broadcast(this);
}
void UCommissionObjective::ClearHoldTimer()
{
if (HoldTimerHandle.IsValid() && Player)
Player->GetWorldTimerManager().ClearTimer(HoldTimerHandle);
HoldTimerHandle.Invalidate();
HoldStartTime = 0.0f;
}
FText UCommissionObjective::GetDescription() const
{
return FText::GetEmpty();
}
float UCommissionObjective::GetProgress() const
{
if (bSatisfied)
return 1.0f;
if (HoldTimerHandle.IsValid() && Player && RequiredHoldSeconds > 0.0f)
{
const float Elapsed = Player->GetWorld()->GetTimeSeconds() - HoldStartTime;
return FMath::Clamp(Elapsed / RequiredHoldSeconds, 0.0f, 1.0f);
}
return 0.0f;
}
@@ -0,0 +1,77 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "CommissionObjective.generated.h"
class ANakedDesireCharacter;
class UCommissionObjective;
class UCommissionConstraint;
DECLARE_MULTICAST_DELEGATE_OneParam(FOnObjectiveStateChangedSignature, UCommissionObjective*);
/**
* One typed objective step (GDD §13.4). Replaces the old Goal/Restriction split: a step owns its own
* condition and, optionally, a sustained-hold requirement (RequiredHoldSeconds). Subclasses override
* IsConditionMet() and call NotifyConditionChanged() when their inputs change; count-style objectives
* can instead call MarkSatisfied() directly. The hold timer lives here, so "expose for N seconds" and
* "expose once" differ only by a data value, not a class.
*/
UCLASS(Abstract, EditInlineNew, BlueprintType)
class NAKEDDESIRE_API UCommissionObjective : public UObject
{
GENERATED_BODY()
public:
FOnObjectiveStateChangedSignature OnStateChanged;
void Activate(ANakedDesireCharacter* InPlayer);
void Deactivate();
bool IsSatisfied() const { return bSatisfied; }
UFUNCTION(BlueprintPure)
virtual FText GetDescription() const;
// 0..1 for UI. Default: 1 when satisfied, partial while a hold timer runs, else 0.
UFUNCTION(BlueprintPure)
virtual float GetProgress() const;
protected:
UPROPERTY()
ANakedDesireCharacter* Player = nullptr;
// Continuous seconds the condition must hold to satisfy. 0 = satisfied the instant it is first met.
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0))
float RequiredHoldSeconds = 0.0f;
// "While Y" gates: the objective only counts progress while every constraint reports IsMet().
UPROPERTY(EditDefaultsOnly, Instanced)
TArray<TObjectPtr<UCommissionConstraint>> Constraints;
virtual void OnActivate() {}
virtual void OnDeactivate() {}
// Sustained objectives override this; the base ANDs it with the constraints to drive the hold timer.
virtual bool IsConditionMet() const { return false; }
// True when every attached constraint is currently satisfied (count-style subclasses gate on this too).
bool AreConstraintsMet() const;
// Re-check the condition and (re)arm or cancel the hold timer accordingly.
void NotifyConditionChanged();
void MarkSatisfied();
bool bSatisfied = false;
private:
void HandleConstraintChanged();
void OnHoldElapsed();
void ClearHoldTimer();
FTimerHandle HoldTimerHandle;
float HoldStartTime = 0.0f;
};
@@ -0,0 +1,54 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CommissionTypes.generated.h"
// Daily vs. weekly draw from the same model; the tier only changes reward bands / step depth (§13.4).
UENUM(BlueprintType)
enum class ECommissionTier : uint8
{
Daily,
Weekly
};
// Lifecycle state (GDD §13.1 / §13.2). Offered commissions on the board carry no obligation until accepted.
UENUM(BlueprintType)
enum class ECommissionState : uint8
{
Offered, // on the board, not committed
Accepted, // committed; objectives armed
Completed, // all objectives satisfied; rewards paid
Expired // accepted but not finished by the deadline
};
// Reward paid instantly on completion (§23 #23). Money/XP exist today; followers land with Phase 8.
USTRUCT(BlueprintType)
struct NAKEDDESIRE_API FCommissionReward
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 Money = 0;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
float XP = 0.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 Followers = 0;
};
// Persisted commission state, keyed by the authored CommissionId. State-level only — objective
// mid-progress is not saved; an accepted commission re-arms from the start on load.
USTRUCT()
struct NAKEDDESIRE_API FCommissionSaveRecord
{
GENERATED_BODY()
UPROPERTY(SaveGame)
FName CommissionId;
UPROPERTY(SaveGame)
ECommissionState State = ECommissionState::Offered;
};
@@ -0,0 +1,46 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "DayPhaseConstraint.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Commissions.Constraints.DayPhase"
void UDayPhaseConstraint::OnActivate()
{
if (UTimeOfDaySubsystem* Time = GetTime())
Time->OnPhaseChanged.AddUniqueDynamic(this, &UDayPhaseConstraint::HandlePhaseChanged);
}
void UDayPhaseConstraint::OnDeactivate()
{
if (UTimeOfDaySubsystem* Time = GetTime())
Time->OnPhaseChanged.RemoveDynamic(this, &UDayPhaseConstraint::HandlePhaseChanged);
}
bool UDayPhaseConstraint::IsMet() const
{
const UTimeOfDaySubsystem* Time = GetTime();
return Time && Time->GetPhase() == RequiredPhase;
}
void UDayPhaseConstraint::HandlePhaseChanged(EDayPhase NewPhase)
{
NotifyChanged();
}
UTimeOfDaySubsystem* UDayPhaseConstraint::GetTime() const
{
UWorld* World = Player ? Player->GetWorld() : nullptr;
return World ? World->GetSubsystem<UTimeOfDaySubsystem>() : nullptr;
}
FText UDayPhaseConstraint::GetDescription() const
{
return (RequiredPhase == EDayPhase::Night)
? LOCTEXT("Night", "at night")
: LOCTEXT("Day", "during the day");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,32 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Commissions/CommissionConstraint.h"
#include "NakedDesire/Global/TimeOfDaySubsystem.h"
#include "DayPhaseConstraint.generated.h"
// Holds only during the chosen day phase (§10.1).
UCLASS(EditInlineNew, DisplayName = "During Day Phase")
class NAKEDDESIRE_API UDayPhaseConstraint : public UCommissionConstraint
{
GENERATED_BODY()
public:
virtual bool IsMet() const override;
virtual FText GetDescription() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
private:
UPROPERTY(EditDefaultsOnly)
EDayPhase RequiredPhase = EDayPhase::Night;
UFUNCTION()
void HandlePhaseChanged(EDayPhase NewPhase);
UTimeOfDaySubsystem* GetTime() const;
};
@@ -0,0 +1,51 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "LocationConstraint.h"
#include "NakedDesire/Locations/LocationSubsystem.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Commissions.Constraints.Location"
void ULocationConstraint::OnActivate()
{
if (ULocationSubsystem* Locations = GetLocations())
{
Locations->OnLocationEntered.AddUniqueDynamic(this, &ULocationConstraint::HandleLocationChanged);
Locations->OnLocationExited.AddUniqueDynamic(this, &ULocationConstraint::HandleLocationChanged);
}
}
void ULocationConstraint::OnDeactivate()
{
if (ULocationSubsystem* Locations = GetLocations())
{
Locations->OnLocationEntered.RemoveDynamic(this, &ULocationConstraint::HandleLocationChanged);
Locations->OnLocationExited.RemoveDynamic(this, &ULocationConstraint::HandleLocationChanged);
}
}
bool ULocationConstraint::IsMet() const
{
const ULocationSubsystem* Locations = GetLocations();
return Locations && Locations->IsPlayerInLocation(RequiredLocation);
}
void ULocationConstraint::HandleLocationChanged(ULocationData* Location)
{
NotifyChanged();
}
ULocationSubsystem* ULocationConstraint::GetLocations() const
{
UWorld* World = Player ? Player->GetWorld() : nullptr;
return World ? World->GetSubsystem<ULocationSubsystem>() : nullptr;
}
FText ULocationConstraint::GetDescription() const
{
return FText::Format(LOCTEXT("AtLocation", "while at {0}"), FText::FromName(RequiredLocation.GetTagName()));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,35 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "NakedDesire/Commissions/CommissionConstraint.h"
#include "LocationConstraint.generated.h"
class ULocationData;
class ULocationSubsystem;
// Holds while the player occupies a location matching (or nesting under) RequiredLocation.
UCLASS(EditInlineNew, DisplayName = "While At Location")
class NAKEDDESIRE_API ULocationConstraint : public UCommissionConstraint
{
GENERATED_BODY()
public:
virtual bool IsMet() const override;
virtual FText GetDescription() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
private:
UPROPERTY(EditDefaultsOnly)
FGameplayTag RequiredLocation;
UFUNCTION()
void HandleLocationChanged(ULocationData* Location);
ULocationSubsystem* GetLocations() const;
};
@@ -0,0 +1,43 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "ObservedConstraint.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/Stats/StatsManager.h"
#define LOCTEXT_NAMESPACE "Commissions.Constraints.Observed"
void UObservedConstraint::OnActivate()
{
if (Player && Player->StatsManager)
Player->StatsManager->OnObserversChanged.AddUniqueDynamic(this, &UObservedConstraint::HandleObserversChanged);
}
void UObservedConstraint::OnDeactivate()
{
if (Player && Player->StatsManager)
Player->StatsManager->OnObserversChanged.RemoveDynamic(this, &UObservedConstraint::HandleObserversChanged);
}
bool UObservedConstraint::IsMet() const
{
if (!Player || !Player->StatsManager)
return false;
return Player->StatsManager->GetObserverCount() >= MinObservers;
}
void UObservedConstraint::HandleObserversChanged()
{
NotifyChanged();
}
FText UObservedConstraint::GetDescription() const
{
return (MinObservers == 1)
? LOCTEXT("Single", "while someone is watching")
: FText::Format(LOCTEXT("Multiple", "while {0} people are watching"), FText::AsNumber(MinObservers));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,29 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Commissions/CommissionConstraint.h"
#include "ObservedConstraint.generated.h"
// Holds only while at least MinObservers NPCs are perceiving the player (the embarrassment observer set).
UCLASS(EditInlineNew, DisplayName = "While Observed By N NPCs")
class NAKEDDESIRE_API UObservedConstraint : public UCommissionConstraint
{
GENERATED_BODY()
public:
virtual bool IsMet() const override;
virtual FText GetDescription() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
private:
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
int32 MinObservers = 1;
UFUNCTION()
void HandleObserversChanged();
};
@@ -0,0 +1,49 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "WearingSlotConstraint.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Commissions.Constraints.WearingSlot"
void UWearingSlotConstraint::OnActivate()
{
if (Player && Player->ClothingManager)
{
Player->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
Player->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
}
}
void UWearingSlotConstraint::OnDeactivate()
{
if (Player && Player->ClothingManager)
{
Player->ClothingManager->OnClothingEquip.RemoveDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
Player->ClothingManager->OnClothingUnequip.RemoveDynamic(this, &UWearingSlotConstraint::HandleClothingChanged);
}
}
bool UWearingSlotConstraint::IsMet() const
{
if (!Player || !Player->ClothingManager)
return false;
return Player->ClothingManager->IsClothingTypeOn(Slot) == bMustBeWorn;
}
void UWearingSlotConstraint::HandleClothingChanged(UClothingItemInstance* ClothingItemInstance)
{
NotifyChanged();
}
FText UWearingSlotConstraint::GetDescription() const
{
return bMustBeWorn
? LOCTEXT("Wearing", "while dressed in the required item")
: LOCTEXT("NotWearing", "while that slot is bare");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,36 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Clothing/ClothingSlotType.h"
#include "NakedDesire/Commissions/CommissionConstraint.h"
#include "WearingSlotConstraint.generated.h"
class UClothingItemInstance;
// Holds while the chosen slot is occupied (bMustBeWorn) or empty (!bMustBeWorn). Covers both
// "while wearing a coat" (Outerwear occupied) and "while not wearing a top" (Top empty).
UCLASS(EditInlineNew, DisplayName = "While Wearing / Not Wearing Slot")
class NAKEDDESIRE_API UWearingSlotConstraint : public UCommissionConstraint
{
GENERATED_BODY()
public:
virtual bool IsMet() const override;
virtual FText GetDescription() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
private:
UPROPERTY(EditDefaultsOnly)
EClothingSlotType Slot = EClothingSlotType::Outerwear;
UPROPERTY(EditDefaultsOnly)
bool bMustBeWorn = true;
UFUNCTION()
void HandleClothingChanged(UClothingItemInstance* ClothingItemInstance);
};
@@ -0,0 +1,216 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "MissionSubsystem.h"
#include "Commission.h"
#include "CommissionBoardConfig.h"
#include "CommissionTypes.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Global/NakedDesireGameInstance.h"
#include "NakedDesire/Global/TimeOfDaySubsystem.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"
void UMissionSubsystem::OnWorldBeginPlay(UWorld& InWorld)
{
Super::OnWorldBeginPlay(InWorld);
if (UTimeOfDaySubsystem* Time = InWorld.GetSubsystem<UTimeOfDaySubsystem>())
Time->OnDayChanged.AddUniqueDynamic(this, &UMissionSubsystem::HandleDayChanged);
BuildBoard();
RestoreFromSave();
OnBoardChanged.Broadcast();
}
void UMissionSubsystem::Deinitialize()
{
if (const UWorld* World = GetWorld())
{
if (UTimeOfDaySubsystem* Time = World->GetSubsystem<UTimeOfDaySubsystem>())
Time->OnDayChanged.RemoveDynamic(this, &UMissionSubsystem::HandleDayChanged);
}
Super::Deinitialize();
}
void UMissionSubsystem::AcceptCommission(UCommission* Commission)
{
if (!Commission || !OfferedCommissions.Contains(Commission))
return;
ANakedDesireCharacter* Player = GetPlayer();
if (!Player)
return;
// Move to Accepted before arming: Accept() may complete synchronously (objectives already met),
// and HandleCommissionCompleted expects the commission to be in AcceptedCommissions.
OfferedCommissions.Remove(Commission);
AcceptedCommissions.Add(Commission);
Commission->OnCompleted.AddUObject(this, &UMissionSubsystem::HandleCommissionCompleted);
Commission->Accept(Player);
PersistState();
OnBoardChanged.Broadcast();
}
void UMissionSubsystem::AbandonCommission(UCommission* Commission)
{
if (!Commission || !AcceptedCommissions.Contains(Commission))
return;
Commission->OnCompleted.RemoveAll(this);
Commission->Abandon();
AcceptedCommissions.Remove(Commission);
OfferedCommissions.Add(Commission);
PersistState();
OnBoardChanged.Broadcast();
}
void UMissionSubsystem::HandleDayChanged(int32 NewDay)
{
// Day rolls expire anything still accepted, then a fresh board is offered. No RestoreFromSave here:
// the persisted records are only meaningful for a load, and PersistState below clears them.
ExpireAccepted();
BuildBoard();
PersistState();
OnBoardChanged.Broadcast();
}
void UMissionSubsystem::BuildBoard()
{
OfferedCommissions.Reset();
const UCommissionBoardConfig* Config = GetBoardConfig();
if (!Config)
return;
for (UCommission* Authored : Config->Commissions)
{
if (!Authored)
continue;
// Duplicate so each run gets fresh objective state and an outer in this world.
UCommission* Runtime = DuplicateObject<UCommission>(Authored, this);
OfferedCommissions.Add(Runtime);
}
}
void UMissionSubsystem::RestoreFromSave()
{
const UGlobalSaveGameData* Save = GetSave();
if (!Save)
return;
ANakedDesireCharacter* Player = GetPlayer();
for (const FCommissionSaveRecord& Record : Save->GetCommissionRecords())
{
UCommission** Found = OfferedCommissions.FindByPredicate(
[&Record](const UCommission* C) { return C && C->CommissionId == Record.CommissionId; });
if (!Found || !*Found)
continue;
UCommission* Commission = *Found;
if (Record.State == ECommissionState::Accepted)
{
OfferedCommissions.Remove(Commission);
AcceptedCommissions.Add(Commission);
Commission->OnCompleted.AddUObject(this, &UMissionSubsystem::HandleCommissionCompleted);
Commission->RestoreState(ECommissionState::Accepted, Player);
}
else if (Record.State == ECommissionState::Completed)
{
OfferedCommissions.Remove(Commission);
CompletedCommissions.Add(Commission);
Commission->RestoreState(ECommissionState::Completed, Player);
}
}
}
void UMissionSubsystem::ExpireAccepted()
{
for (UCommission* Commission : AcceptedCommissions)
{
if (!Commission)
continue;
Commission->OnCompleted.RemoveAll(this);
Commission->Expire();
// TODO(§13.4): apply failurePenalty here once reputation / followers exist.
}
AcceptedCommissions.Reset();
CompletedCommissions.Reset();
}
void UMissionSubsystem::HandleCommissionCompleted(UCommission* Commission)
{
AcceptedCommissions.Remove(Commission);
CompletedCommissions.AddUnique(Commission);
ApplyReward(Commission->GetReward());
PersistState();
OnCommissionCompleted.Broadcast(Commission);
OnBoardChanged.Broadcast();
}
void UMissionSubsystem::ApplyReward(const FCommissionReward& Reward)
{
// Money wires instantly to the save (§23 #23).
if (UGlobalSaveGameData* Save = GetSave())
Save->Money += Reward.Money;
// XP credits to the shared pool (currently a float on the character; §7.10 GAS migration later).
if (ANakedDesireCharacter* Player = GetPlayer())
Player->XP += Reward.XP;
// TODO: followers — no follower / profile system yet (Phase 8); Reward.Followers is dropped for now.
}
void UMissionSubsystem::PersistState() const
{
UGlobalSaveGameData* Save = GetSave();
if (!Save)
return;
TArray<FCommissionSaveRecord> Records;
for (const UCommission* Commission : AcceptedCommissions)
{
if (Commission)
Records.Add({ Commission->CommissionId, ECommissionState::Accepted });
}
for (const UCommission* Commission : CompletedCommissions)
{
if (Commission)
Records.Add({ Commission->CommissionId, ECommissionState::Completed });
}
Save->SetCommissionRecords(Records);
}
ANakedDesireCharacter* UMissionSubsystem::GetPlayer() const
{
return Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
}
UGlobalSaveGameData* UMissionSubsystem::GetSave() const
{
UGameInstance* GameInstance = GetWorld() ? GetWorld()->GetGameInstance() : nullptr;
USaveSubsystem* SaveSubsystem = GameInstance ? GameInstance->GetSubsystem<USaveSubsystem>() : nullptr;
return SaveSubsystem ? SaveSubsystem->GetCurrentSave() : nullptr;
}
UCommissionBoardConfig* UMissionSubsystem::GetBoardConfig() const
{
const UNakedDesireGameInstance* GameInstance =
Cast<UNakedDesireGameInstance>(GetWorld() ? GetWorld()->GetGameInstance() : nullptr);
return GameInstance ? GameInstance->CommissionBoard : nullptr;
}
@@ -0,0 +1,74 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "MissionSubsystem.generated.h"
class UCommission;
class UCommissionBoardConfig;
class ANakedDesireCharacter;
class UGlobalSaveGameData;
struct FCommissionReward;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCommissionBoardChangedSignature);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCommissionCompletedSignature, UCommission*, Commission);
/**
* Runtime owner of the commission board (GDD §13). Offers the hand-authored pool, drives the
* accept / complete / expire lifecycle, pays rewards instantly on completion (§23 #23), and persists
* commission state to the save. A WorldSubsystem (like UTimeOfDaySubsystem / SessionManager): it needs
* world access + OnDayChanged, and rehydrates durable state from the GameInstance save each level.
*/
UCLASS()
class NAKEDDESIRE_API UMissionSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
virtual void Deinitialize() override;
UFUNCTION(BlueprintPure)
const TArray<UCommission*>& GetOfferedCommissions() const { return OfferedCommissions; }
UFUNCTION(BlueprintPure)
const TArray<UCommission*>& GetAcceptedCommissions() const { return AcceptedCommissions; }
UFUNCTION(BlueprintCallable)
void AcceptCommission(UCommission* Commission);
UFUNCTION(BlueprintCallable)
void AbandonCommission(UCommission* Commission);
UPROPERTY(BlueprintAssignable)
FOnCommissionBoardChangedSignature OnBoardChanged;
UPROPERTY(BlueprintAssignable)
FOnCommissionCompletedSignature OnCommissionCompleted;
private:
UFUNCTION()
void HandleDayChanged(int32 NewDay);
void BuildBoard(); // instantiate offered commissions from the config
void RestoreFromSave(); // re-apply persisted accepted / completed states
void ExpireAccepted(); // deadline: accepted & unfinished -> expired
void HandleCommissionCompleted(UCommission* Commission);
void ApplyReward(const FCommissionReward& Reward);
void PersistState() const;
ANakedDesireCharacter* GetPlayer() const;
UGlobalSaveGameData* GetSave() const;
UCommissionBoardConfig* GetBoardConfig() const;
UPROPERTY()
TArray<UCommission*> OfferedCommissions;
UPROPERTY()
TArray<UCommission*> AcceptedCommissions;
UPROPERTY()
TArray<UCommission*> CompletedCommissions;
};
@@ -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;
};
@@ -6,6 +6,7 @@
#include "NakedDesireGameInstance.generated.h"
class UStartingSaveData;
class UCommissionBoardConfig;
UCLASS()
class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance
@@ -15,4 +16,8 @@ class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance
public:
UPROPERTY(EditDefaultsOnly, Category = "Save")
TObjectPtr<UStartingSaveData> StartingSaveData;
// Hand-authored commission pool the UMissionSubsystem offers (§13).
UPROPERTY(EditDefaultsOnly, Category = "Commissions")
TObjectPtr<UCommissionBoardConfig> CommissionBoard;
};
@@ -0,0 +1,6 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "NakedDesireGameplayTags.h"
UE_DEFINE_GAMEPLAY_TAG(TAG_Location_Apartment, "Location.Apartment");
@@ -0,0 +1,10 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "NativeGameplayTags.h"
// Native gameplay tags shared across systems. The apartment is identified by its ULocationData tag
// matching (or nesting under) Location.Apartment — that is how the session boundary is detected now,
// replacing the old per-trigger bIsApartment flag.
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Location_Apartment);
@@ -5,6 +5,9 @@
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Global/Constants.h"
#include "NakedDesire/Global/NakedDesireGameplayTags.h"
#include "NakedDesire/Locations/LocationData.h"
#include "NakedDesire/Locations/LocationSubsystem.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/Stats/StatsManager.h"
@@ -12,6 +15,13 @@ void USessionManagerSubsystem::OnWorldBeginPlay(UWorld& InWorld)
{
Super::OnWorldBeginPlay(InWorld);
// The apartment threshold is now a location event (Location.Apartment), not a per-trigger flag.
if (ULocationSubsystem* Locations = InWorld.GetSubsystem<ULocationSubsystem>())
{
Locations->OnLocationEntered.AddDynamic(this, &USessionManagerSubsystem::HandleLocationEntered);
Locations->OnLocationExited.AddDynamic(this, &USessionManagerSubsystem::HandleLocationExited);
}
// The player pawn and its UStatsManager may not have finished BeginPlay when
// the world begins play, so defer binding by one tick.
InWorld.GetTimerManager().SetTimerForNextTick(this, &USessionManagerSubsystem::BindToPlayerStats);
@@ -30,19 +40,19 @@ void USessionManagerSubsystem::BindToPlayerStats()
Player->StatsManager->EnergyUpdate.AddDynamic(this, &USessionManagerSubsystem::HandleEnergyUpdate);
}
void USessionManagerSubsystem::NotifyEnteredApartment()
void USessionManagerSubsystem::HandleLocationEntered(ULocationData* Location)
{
// Returning home is the safe end of a session (§4.3).
if (bSessionActive)
// Returning to the apartment is the safe end of a session (§4.3).
if (Location && Location->Tag.MatchesTag(TAG_Location_Apartment) && bSessionActive)
{
EndSession(ESessionLossCause::SafeReturn);
}
}
void USessionManagerSubsystem::NotifyLeftApartment()
void USessionManagerSubsystem::HandleLocationExited(ULocationData* Location)
{
// Leaving the apartment is the only way to start a session (§4.1).
if (!bSessionActive)
if (Location && Location->Tag.MatchesTag(TAG_Location_Apartment) && !bSessionActive)
{
StartSession();
}
@@ -7,6 +7,7 @@
#include "SessionManagerSubsystem.generated.h"
class UStatsManager;
class ULocationData;
/**
* Why a session ended (GDD §4.4). SafeReturn is a non-loss end (player walked
@@ -33,6 +34,10 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionEndSignature, ESessionLoss
* ALocationTrigger drives start / end; embarrassment-max and energy-zero are
* detected by subscribing to UStatsManager. This replaces the EndGameEmbarrassed
* Blueprint event on ANakedDesireGameMode.
*
* The apartment threshold is detected via ULocationSubsystem: a session starts when the player leaves
* a location tagged Location.Apartment and ends (SafeReturn) when they re-enter one. There is no
* per-trigger apartment flag — the apartment is just a tagged location.
*/
UCLASS()
class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem
@@ -42,10 +47,6 @@ class NAKEDDESIRE_API USessionManagerSubsystem : public UWorldSubsystem
public:
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
// Called by the apartment ALocationTrigger as the player crosses the threshold.
void NotifyEnteredApartment();
void NotifyLeftApartment();
UFUNCTION(BlueprintPure, Category = "Session")
bool IsSessionActive() const { return bSessionActive; }
@@ -69,6 +70,12 @@ private:
void BindToPlayerStats();
UFUNCTION()
void HandleLocationEntered(ULocationData* Location);
UFUNCTION()
void HandleLocationExited(ULocationData* Location);
UFUNCTION()
void HandleEmbarrassmentUpdate(float CurrentValue, float MaxValue);
@@ -0,0 +1,76 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "LocationSubsystem.h"
#include "LocationData.h"
void ULocationSubsystem::EnterLocation(ULocationData* Location)
{
if (!Location)
return;
int32& Count = ActiveCounts.FindOrAdd(Location);
++Count;
if (Count == 1)
{
RebuildActiveTags();
OnLocationEntered.Broadcast(Location);
}
}
void ULocationSubsystem::ExitLocation(ULocationData* Location)
{
if (!Location)
return;
int32* Count = ActiveCounts.Find(Location);
if (!Count)
return;
if (--(*Count) <= 0)
{
ActiveCounts.Remove(Location);
RebuildActiveTags();
OnLocationExited.Broadcast(Location);
}
}
bool ULocationSubsystem::IsPlayerInLocation(FGameplayTag Query) const
{
return Query.IsValid() && ActiveTags.HasTag(Query);
}
ULocationData* ULocationSubsystem::GetCurrentLocation() const
{
ULocationData* Best = nullptr;
int32 BestDepth = -1;
for (const TPair<TObjectPtr<ULocationData>, int32>& Pair : ActiveCounts)
{
ULocationData* Location = Pair.Key;
if (!Location || !Location->Tag.IsValid())
continue;
// More tag segments = more specific (Location.City.Beach beats Location.City).
const int32 Depth = Location->Tag.GetGameplayTagParents().Num();
if (Depth > BestDepth)
{
BestDepth = Depth;
Best = Location;
}
}
return Best;
}
void ULocationSubsystem::RebuildActiveTags()
{
ActiveTags.Reset();
for (const TPair<TObjectPtr<ULocationData>, int32>& Pair : ActiveCounts)
{
if (Pair.Key && Pair.Key->Tag.IsValid())
ActiveTags.AddTag(Pair.Key->Tag);
}
}
@@ -0,0 +1,58 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "Subsystems/WorldSubsystem.h"
#include "LocationSubsystem.generated.h"
class ULocationData;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLocationChangedSignature, ULocationData*, Location);
/**
* Single source of truth for which tagged locations the player currently occupies (GDD §10.4).
* Fed by ALocationTrigger overlaps; queried + subscribed by everything else (session boundary,
* commission location constraints, future shop entry). World-scoped and transient — nothing to save.
*
* Locations are identified by their ULocationData FGameplayTag, so they nest: while inside
* Location.City.Beach the player also matches Location.City. The player can be in several at once.
*/
UCLASS()
class NAKEDDESIRE_API ULocationSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
// Reported by ALocationTrigger as the player enters / leaves a tagged volume.
void EnterLocation(ULocationData* Location);
void ExitLocation(ULocationData* Location);
// True while the player occupies a location whose tag matches (or nests under) Query.
UFUNCTION(BlueprintPure, Category = "Location")
bool IsPlayerInLocation(FGameplayTag Query) const;
UFUNCTION(BlueprintPure, Category = "Location")
FGameplayTagContainer GetPlayerLocationTags() const { return ActiveTags; }
// Most-specific current location (deepest tag) — for HUD / prompts. Null when outdoors / untagged.
UFUNCTION(BlueprintPure, Category = "Location")
ULocationData* GetCurrentLocation() const;
UPROPERTY(BlueprintAssignable, Category = "Location")
FOnLocationChangedSignature OnLocationEntered;
UPROPERTY(BlueprintAssignable, Category = "Location")
FOnLocationChangedSignature OnLocationExited;
private:
void RebuildActiveTags();
// Ref-counted: a location can be several overlapping trigger volumes, so we only fire enter on
// 0->1 and exit on 1->0 — crossing between two boxes of the same place doesn't churn events.
UPROPERTY()
TMap<TObjectPtr<ULocationData>, int32> ActiveCounts;
FGameplayTagContainer ActiveTags;
};
@@ -1,10 +1,10 @@
// © 2025 Naked People Team. All Rights Reserved.
// © 2025 Naked People Team. All Rights Reserved.
#include "LocationTrigger.h"
#include "Components/BoxComponent.h"
#include "NakedDesire/Global/SessionManagerSubsystem.h"
#include "LocationSubsystem.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
@@ -25,11 +25,8 @@ void ALocationTrigger::BeginPlay()
{
Super::BeginPlay();
if (bIsApartment)
{
BoxTrigger->OnComponentBeginOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerBeginOverlap);
BoxTrigger->OnComponentEndOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerEndOverlap);
}
BoxTrigger->OnComponentBeginOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerBeginOverlap);
BoxTrigger->OnComponentEndOverlap.AddDynamic(this, &ALocationTrigger::OnTriggerEndOverlap);
}
void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
@@ -38,10 +35,8 @@ void ALocationTrigger::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComp
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
return;
if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem<USessionManagerSubsystem>())
{
SessionManager->NotifyEnteredApartment();
}
if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem<ULocationSubsystem>())
Locations->EnterLocation(LocationData);
}
void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
@@ -50,9 +45,6 @@ void ALocationTrigger::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedCompon
if (!OtherActor || !OtherActor->IsA<ANakedDesireCharacter>())
return;
if (USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem<USessionManagerSubsystem>())
{
SessionManager->NotifyLeftApartment();
}
}
if (ULocationSubsystem* Locations = GetWorld()->GetSubsystem<ULocationSubsystem>())
Locations->ExitLocation(LocationData);
}
@@ -1,4 +1,4 @@
// © 2025 Naked People Team. All Rights Reserved.
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
@@ -9,6 +9,9 @@
class ULocationData;
class UBoxComponent;
// A tagged volume. On player overlap it reports enter / leave to ULocationSubsystem, which is the
// single authority on where the player is. Everything (session boundary, commission location
// constraints, etc.) consumes the subsystem — the trigger itself has no consumer-specific logic.
UCLASS()
class NAKEDDESIRE_API ALocationTrigger : public AActor
{
@@ -20,12 +23,6 @@ class NAKEDDESIRE_API ALocationTrigger : public AActor
UPROPERTY(EditAnywhere)
ULocationData* LocationData;
// When set, the player crossing this trigger drives session start / end on
// USessionManagerSubsystem (GDD §4.1 / §4.3). Exactly one trigger — the
// apartment — should have this checked.
UPROPERTY(EditAnywhere, Category = "Session")
bool bIsApartment = false;
public:
ALocationTrigger();
@@ -42,4 +39,4 @@ private:
UFUNCTION()
void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
};
};
@@ -1,27 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "GoalRestriction.h"
void UGoalRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
{
IsSuccess = false;
Player = PlayerCharacter;
OnUpdate.Broadcast(this);
}
void UGoalRestriction::Complete()
{
Complete(true);
}
void UGoalRestriction::Complete(const bool Value)
{
IsSuccess = Value;
OnUpdate.Broadcast(this);
}
FText UGoalRestriction::GetDescription() const
{
return FText::GetEmpty();
}
@@ -1,44 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "GoalRestriction.generated.h"
class ANakedDesireCharacter;
class UGoalRestriction;
DECLARE_MULTICAST_DELEGATE_OneParam(FMissionRestrictionUpdateSignature, UGoalRestriction*);
/**
*
*/
UCLASS(EditInlineNew, BlueprintType)
class NAKEDDESIRE_API UGoalRestriction : public UObject
{
GENERATED_BODY()
public:
FMissionRestrictionUpdateSignature OnUpdate;
UFUNCTION(BlueprintPure)
bool GetIsSuccess() const
{
return IsSuccess;
}
virtual void Init(ANakedDesireCharacter* PlayerCharacter);
virtual void Complete();
virtual void Complete(bool Value);
virtual void Stop() {};
UFUNCTION(BlueprintPure)
virtual FText GetDescription() const;
protected:
UPROPERTY()
ANakedDesireCharacter* Player = nullptr;
bool IsSuccess = false;
};
@@ -1,81 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "FlashGoal.h"
#include "NakedDesire/NPC/NPCAIController.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Missions.Goals.Flash"
const FText GoalsFlashSingle = LOCTEXT("Description.Single", "Flash someone {BodyPart} once");
const FText GoalsFlashMultiple = LOCTEXT("Description.Multiple", "Flash {BodyPart} to {PeopleCount} people");
#undef LOCTEXT_NAMESPACE
void UFlashGoal::Init(ANakedDesireCharacter* PlayerCharacter)
{
Super::Init(PlayerCharacter);
PlayerNoticedHandle = PlayerCharacter->OnNoticed.AddUObject(this, &UFlashGoal::OnPlayerNoticed);
}
void UFlashGoal::Complete()
{
Super::Complete();
Player->OnNoticed.Remove(PlayerNoticedHandle);
}
FText UFlashGoal::GetDescription() const
{
FText BodyTypeString;
switch (BodyType)
{
case EBodyPart::Ass:
BodyTypeString = FText::FromString("Ass");
break;
case EBodyPart::Boobs:
BodyTypeString = FText::FromString("Boobs");
break;
case EBodyPart::Genitals:
BodyTypeString = FText::FromString("Genitals");
break;
default:
BodyTypeString = FText::FromString("None");
break;
}
if (RequiredFlashCount == 1)
{
return FText::Format(GoalsFlashSingle,
FFormatNamedArguments
{
{TEXT("BodyPart"), BodyTypeString}
});
}
return FText::Format(GoalsFlashMultiple,
FFormatNamedArguments
{
{TEXT("BodyPart"), BodyTypeString},
{TEXT("PeopleCount"), RequiredFlashCount}
});
}
void UFlashGoal::OnPlayerNoticed(ANPCAIController* NPC)
{
if (IsCompleted || !CheckRestrictions())
{
return;
}
if (!NoticedActors.Contains(NPC))
{
NoticedActors.Add(NPC);
}
if (NoticedActors.Num() >= RequiredFlashCount)
{
Complete();
}
}
@@ -1,37 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Clothing/BodyPart.h"
#include "NakedDesire/MissionBuilder/MissionGoal.h"
#include "FlashGoal.generated.h"
class ANPCAIController;
/**
*
*/
UCLASS(EditInlineNew)
class NAKEDDESIRE_API UFlashGoal : public UMissionGoal
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
int RequiredFlashCount = 1;
UPROPERTY(EditDefaultsOnly)
EBodyPart BodyType;
FDelegateHandle PlayerNoticedHandle;
UPROPERTY()
TSet<AActor*> NoticedActors;
public:
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
virtual void Complete() override;
virtual FText GetDescription() const override;
private:
void OnPlayerNoticed(ANPCAIController* NPC);
};
@@ -1,42 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "MinTimeGoal.h"
#define LOCTEXT_NAMESPACE "Missions.Goals.MinTime"
const FText GoalsMinTimeDescription = LOCTEXT("Description", "Do following at least {MinTime} seconds");
#undef LOCTEXT_NAMESPACE
FText UMinTimeGoal::GetDescription() const
{
return FText::Format(GoalsMinTimeDescription,
FFormatNamedArguments
{
{TEXT("MinTime"), MinTime}
});
}
void UMinTimeGoal::OnRestrictionUpdated(UGoalRestriction* Restriction)
{
Super::OnRestrictionUpdated(Restriction);
if (CheckRestrictions())
{
if (!TimerHandle.IsValid())
{
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UMinTimeGoal::TimeIsUp, MinTime, false);
}
}
else
{
if (TimerHandle.IsValid())
{
GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}
}
}
void UMinTimeGoal::TimeIsUp()
{
Complete();
}
@@ -1,30 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "../MissionGoal.h"
#include "MinTimeGoal.generated.h"
/**
*
*/
UCLASS(EditInlineNew)
class NAKEDDESIRE_API UMinTimeGoal : public UMissionGoal
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual void OnRestrictionUpdated(UGoalRestriction* Restriction) override;
private:
UPROPERTY(EditDefaultsOnly)
float MinTime = 3.f;
FTimerHandle TimerHandle;
void TimeIsUp();
};
@@ -1,63 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "Mission.h"
#include "MissionGoal.h"
void UMission::Init(ANakedDesireCharacter* PlayerCharacter)
{
Player = PlayerCharacter;
for (const auto& Goal : Goals)
{
Goal->Init(Player);
auto Handle = Goal->OnUpdate.AddUObject(this, &UMission::OnGoalUpdated);
GoalUpdateHandles.Add(Handle);
}
}
void UMission::OnGoalUpdated(UMissionGoal* MissionGoal)
{
if (IsCompleted)
{
return;
}
if (CheckGoals())
{
Complete();
}
}
void UMission::Complete()
{
IsCompleted = true;
for (const auto& Goal : Goals)
{
for (const auto& Handle : GoalUpdateHandles)
{
Goal->OnUpdate.Remove(Handle);
}
}
OnComplete.Broadcast(this);
}
bool UMission::CheckGoals()
{
if (IsCompleted)
{
return true;
}
for (const auto& Goal : Goals)
{
if (!Goal->GetIsCompleted() || !Goal->CheckRestrictions())
{
return false;
}
}
return true;
}
@@ -1,67 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "Mission.generated.h"
DECLARE_MULTICAST_DELEGATE_OneParam(FMissionCompleteSignature, class UMission*);
class UMissionGoal;
class ANakedDesireCharacter;
class UGoalRestriction;
/**
*
*/
UCLASS(EditInlineNew, BlueprintType)
class NAKEDDESIRE_API UMission : public UObject
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, Instanced)
TArray<UMissionGoal*> Goals;
UPROPERTY(EditDefaultsOnly)
int MoneyReward = 0;
bool IsCompleted = false;
TArray<FDelegateHandle> GoalUpdateHandles;
public:
FMissionCompleteSignature OnComplete;
void Init(ANakedDesireCharacter* PlayerCharacter);
UFUNCTION(BlueprintPure)
int GetMoneyReward() const
{
return MoneyReward;
}
UFUNCTION(BlueprintPure)
TArray<UMissionGoal*> GetGoals() const
{
return Goals;
}
UFUNCTION(BlueprintPure)
bool GetIsCompleted() const
{
return IsCompleted;
}
protected:
UPROPERTY()
ANakedDesireCharacter* Player = nullptr;
private:
UFUNCTION()
void OnGoalUpdated(UMissionGoal* MissionGoal);
void Complete();
bool CheckGoals();
};
@@ -1,70 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "MissionGoal.h"
#include "GoalRestriction.h"
void UMissionGoal::Init(ANakedDesireCharacter* PlayerCharacter)
{
Player = PlayerCharacter;
for (const auto& Elem : Restrictions)
{
Elem->Init(Player);
auto Handle = Elem->OnUpdate.AddUObject(this, &UMissionGoal::OnRestrictionUpdated);
RestrictionUpdateHandles.Add(Handle);
}
}
void UMissionGoal::Complete()
{
if (!CheckRestrictions())
{
return;
}
IsCompleted = true;
for (const auto& Restriction : Restrictions)
{
Restriction->Stop();
for (const auto& Handle : RestrictionUpdateHandles)
{
Restriction->OnUpdate.Remove(Handle);
}
}
OnUpdate.Broadcast(this);
}
bool UMissionGoal::CheckRestrictions()
{
if (IsCompleted)
{
return true;
}
for (const auto& Restriction : Restrictions)
{
if (!Restriction->GetIsSuccess())
{
return false;
}
}
return true;
}
FText UMissionGoal::GetDescription() const
{
return FText::GetEmpty();
}
void UMissionGoal::OnRestrictionUpdated(UGoalRestriction* Restriction)
{
if (IsCompleted)
{
return;
}
OnUpdate.Broadcast(this);
}
@@ -1,58 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "MissionGoal.generated.h"
class ANakedDesireCharacter;
class UMissionGoal;
class UGoalRestriction;
DECLARE_MULTICAST_DELEGATE_OneParam(FGoalUpdateSignature, UMissionGoal*);
/**
*
*/
UCLASS(EditInlineNew, BlueprintType)
class NAKEDDESIRE_API UMissionGoal : public UObject
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, Instanced)
TArray<UGoalRestriction*> Restrictions;
TArray<FDelegateHandle> RestrictionUpdateHandles;
public:
virtual void Init(ANakedDesireCharacter* PlayerCharacter);
virtual void Complete();
UFUNCTION(BlueprintPure)
bool GetIsCompleted() const
{
return IsCompleted;
}
UFUNCTION(BlueprintPure)
TArray<UGoalRestriction*> GetRestrictions() const
{
return Restrictions;
}
FGoalUpdateSignature OnUpdate;
bool CheckRestrictions();
UFUNCTION(BlueprintPure)
virtual FText GetDescription() const;
protected:
UPROPERTY()
ANakedDesireCharacter* Player = nullptr;
virtual void OnRestrictionUpdated(UGoalRestriction* Restriction);
bool IsCompleted = false;
};
@@ -1,4 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "MissionsConfig.h"
@@ -1,34 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "MissionsConfig.generated.h"
class UMission;
USTRUCT()
struct NAKEDDESIRE_API FMissionsConfigItem
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, Instanced)
TArray<UMission*> Missions;
};
/**
*
*/
UCLASS()
class NAKEDDESIRE_API UMissionsConfig : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly)
TArray<FMissionsConfigItem> DailyMissions;
UPROPERTY(EditDefaultsOnly)
TArray<FMissionsConfigItem> WeeklyMissions;
};
@@ -1,50 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "MissionsManager.h"
#include "Mission.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
UMissionsManager::UMissionsManager()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UMissionsManager::BeginPlay()
{
Super::BeginPlay();
Player = Cast<ANakedDesireCharacter>(GetOwner());
for (const auto& Mission : AvailableMissions)
{
Mission->Init(Player);
Mission->OnComplete.AddUObject(this, &UMissionsManager::CompleteMission);
}
}
void UMissionsManager::CompleteMission(UMission* Mission)
{
CompletedMissions.Add(Mission);
AvailableMissions.Remove(Mission);
OnMissionCompleted.Broadcast(Mission);
}
void UMissionsManager::RefreshDailyMissions(const TArray<UMission*>& NewMissions)
{
for (UMission* Mission : AvailableMissions)
{
Mission->OnComplete.RemoveAll(this);
}
AvailableMissions.Reset();
AvailableMissions.Append(NewMissions);
for (UMission* Mission : AvailableMissions)
{
Mission->Init(Player);
Mission->OnComplete.AddUObject(this, &UMissionsManager::CompleteMission);
}
}
@@ -1,45 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "MissionsManager.generated.h"
class UMission;
class ANakedDesireCharacter;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMissionCompletedSignature, UMission*, Mission);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRewardsCollected, int, Reward);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class NAKEDDESIRE_API UMissionsManager : public UActorComponent
{
GENERATED_BODY()
UPROPERTY()
ANakedDesireCharacter* Player = nullptr;
public:
UMissionsManager();
UPROPERTY(EditDefaultsOnly, Instanced, BlueprintReadOnly)
TArray<UMission*> AvailableMissions;
UPROPERTY(BlueprintReadWrite)
TArray<UMission*> CompletedMissions;
UPROPERTY(BlueprintAssignable)
FOnMissionCompletedSignature OnMissionCompleted;
UPROPERTY(BlueprintAssignable)
FOnRewardsCollected OnRewardsCollected;
void CompleteMission(UMission* Mission);
UFUNCTION()
void RefreshDailyMissions(const TArray<UMission*>& NewMissions);
virtual void BeginPlay() override;
};
@@ -1,109 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "EquipClothingRestriction.h"
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Missions.Restriction.EquipClothing"
const FText RestrictionEquipClothingDescriptionSingle = LOCTEXT("Description.Single", "Equip {ItemName}");
const FText RestrictionEquipClothingDescriptionMultiple = LOCTEXT("Description.Multiple", "Equip one of: {ItemNames}");
#undef LOCTEXT_NAMESPACE
void UEquipClothingRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
{
Super::Init(PlayerCharacter);
if (!ClothingEquippedDelegateHandle.IsValid())
{
Player->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UEquipClothingRestriction::OnClothingEquipped);
}
if (!ClothingUnequippedDelegateHandle.IsValid())
{
Player->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UEquipClothingRestriction::OnClothingUnequipped);
}
CheckClothing();
}
void UEquipClothingRestriction::Stop()
{
Super::Stop();
Player->ClothingManager->OnClothingEquip.Remove(this, TEXT("OnClothingEquipped"));
Player->ClothingManager->OnClothingUnequip.Remove(this, TEXT("OnClothingUnequipped"));
}
FText UEquipClothingRestriction::GetDescription() const
{
if (ClothingItems.Num() == 1 && ClothingItems[0])
{
return FText::Format(RestrictionEquipClothingDescriptionSingle,
FFormatNamedArguments
{
{TEXT("ItemName"), ClothingItems[0]->Name}
});
}
FString Items = TEXT("");
for (const auto& Item : ClothingItems)
{
if (Item)
{
Items += Item->Name.ToString() + ", ";
}
}
return FText::Format(RestrictionEquipClothingDescriptionMultiple,
FFormatNamedArguments
{
{TEXT("ItemNames"), FText::FromString(Items)}
});
}
void UEquipClothingRestriction::OnClothingEquipped(UClothingItemInstance* ClothingItemInstance)
{
const bool IsTargetClothing = ClothingItems.FindByPredicate([&ClothingItemInstance](const UClothingItemDefinition* Item)
{
return Item && Item->Name.EqualTo(ClothingItemInstance->GetClothingItemDefinition()->Name);
}) != nullptr;
if (IsTargetClothing)
{
Complete();
}
}
void UEquipClothingRestriction::OnClothingUnequipped(UClothingItemInstance* ClothingItemInstance)
{
const bool IsTargetClothing = ClothingItems.FindByPredicate([&ClothingItemInstance](const UClothingItemDefinition* Item)
{
return Item && Item->Name.EqualTo(ClothingItemInstance->GetClothingItemDefinition()->Name);
}) != nullptr;
if (IsTargetClothing)
{
Init(Player);
}
}
void UEquipClothingRestriction::CheckClothing()
{
// for (const FClothingSlotData& ClothingSlot : Player->ClothingManager->ClothingSlots)
// {
// if (!ClothingSlot.ClothingItemInstance)
// {
// continue;
// }
//
// const bool IsTargetClothing = ClothingItems.FindByPredicate([&ClothingSlot](const UClothingItem* Item)
// {
// return Item && Item->Name.EqualTo(ClothingSlot.ClothingItemInstance->GetClothingItem()->Name);
// }) != nullptr;
// if (IsTargetClothing)
// {
// Complete();
// return;
// }
// }
}
@@ -1,37 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/MissionBuilder/GoalRestriction.h"
#include "EquipClothingRestriction.generated.h"
class UClothingItemInstance;
class UClothingItemDefinition;
UCLASS(EditInlineNew)
class NAKEDDESIRE_API UEquipClothingRestriction : public UGoalRestriction
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, meta = (ToolTip =
"One of provided clothing items should be equipped by player. If multiple clothing items required provide multiple restrictions"))
TArray<UClothingItemDefinition*> ClothingItems;
public:
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
virtual void Stop() override;
virtual FText GetDescription() const override;
private:
FDelegateHandle ClothingEquippedDelegateHandle;
FDelegateHandle ClothingUnequippedDelegateHandle;
UFUNCTION()
void OnClothingEquipped(UClothingItemInstance* ClothingItemInstance);
UFUNCTION()
void OnClothingUnequipped(UClothingItemInstance* ClothingItemInstance);
void CheckClothing();
};
@@ -1,95 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "ExposeBodyPartRestriction.h"
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#define LOCTEXT_NAMESPACE "Missions.Restrictions.ExposeBodyPart"
const FText RestrictionsExposeBodyPartDescription = LOCTEXT("Description", "Expose {BodyPart}");
#undef LOCTEXT_NAMESPACE
void UExposeBodyPartRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
{
Super::Init(PlayerCharacter);
PlayerCharacter->ClothingManager->OnClothingEquip.AddUniqueDynamic(this, &UExposeBodyPartRestriction::EquipClothing);
PlayerCharacter->ClothingManager->OnClothingUnequip.AddUniqueDynamic(this, &UExposeBodyPartRestriction::UnequipClothing);
CheckClothing();
}
void UExposeBodyPartRestriction::Stop()
{
Super::Stop();
Player->ClothingManager->OnClothingEquip.Remove(this, TEXT("ClothingEquip"));
Player->ClothingManager->OnClothingUnequip.Remove(this, TEXT("ClothingUnequip"));
}
FText UExposeBodyPartRestriction::GetDescription() const
{
FText BodyPartString;
switch (BodyPart)
{
case EBodyPart::Ass:
BodyPartString = FText::FromString("Ass");
break;
case EBodyPart::Boobs:
BodyPartString = FText::FromString("Boobs");
break;
case EBodyPart::Genitals:
BodyPartString = FText::FromString("Genitals");
break;
default:
BodyPartString = FText::FromString("None");
break;
}
return FText::Format(RestrictionsExposeBodyPartDescription,
FFormatNamedArguments
{
{TEXT("BodyPart"), BodyPartString}
});
}
void UExposeBodyPartRestriction::EquipClothing(UClothingItemInstance* ClothingItemInstance)
{
if (IsSuccess) // TODO: Add covered body part resolution
{
Init(Player);
}
}
void UExposeBodyPartRestriction::UnequipClothing(UClothingItemInstance* ClothingItemInstance)
{
if (IsSuccess)
return;
CheckClothing();
}
void UExposeBodyPartRestriction::CheckClothing()
{
// if (!Player || !Player->ClothingManager || Player->ClothingManager->ClothingSlots.IsEmpty())
// {
// return;
// }
//
// const FClothingSlotData* TargetClothingItem = Player->ClothingManager->ClothingSlots.FindByPredicate([this](const FClothingSlotData& ClothingSlot)
// {
// if (ClothingSlot.ClothingItemInstance)
// {
// return true; // TODO: Add exposed body part resolution
// }
//
// return false;
// });
// if (!TargetClothingItem)
// {
// Complete();
// }
}
@@ -1,36 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Clothing/BodyPart.h"
#include "NakedDesire/MissionBuilder/GoalRestriction.h"
#include "ExposeBodyPartRestriction.generated.h"
class UClothingItemInstance;
UCLASS(EditInlineNew)
class NAKEDDESIRE_API UExposeBodyPartRestriction : public UGoalRestriction
{
GENERATED_BODY()
FDelegateHandle EquipClothingHandle;
FDelegateHandle UnequipClothingHandle;
UPROPERTY(EditDefaultsOnly)
EBodyPart BodyPart;
public:
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
virtual void Stop() override;
virtual FText GetDescription() const override;
private:
UFUNCTION()
void EquipClothing(UClothingItemInstance* ClothingItemInstance);
UFUNCTION()
void UnequipClothing(UClothingItemInstance* ClothingItemInstance);
void CheckClothing();
};
@@ -1,51 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "LocationRestriction.h"
#include "NakedDesire/Locations/LocationData.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Missions.Restrictions.Location"
const FText RestrictionsLocationDescription = LOCTEXT("Description", "Visit {LocationName}");
#undef LOCTEXT_NAMESPACE
void ULocationRestriction::Init(ANakedDesireCharacter* PlayerCharacter)
{
Super::Init(PlayerCharacter);
// AreaEnterHandle = PlayerCharacter->OnAreaEnter.AddUObject(this, &ULocationRestriction::OnAreaEnter);
// AreaExitHandle = PlayerCharacter->OnAreaExit.AddUObject(this, &ULocationRestriction::OnAreaExit);
}
void ULocationRestriction::Stop()
{
Super::Stop();
// Player->OnAreaEnter.Remove(AreaEnterHandle);
// Player->OnAreaExit.Remove(AreaExitHandle);
}
FText ULocationRestriction::GetDescription() const
{
return FText::Format(RestrictionsLocationDescription,
FFormatNamedArguments
{
{TEXT("LocationName"), TargetLocation->Name}
});
}
void ULocationRestriction::OnAreaEnter(ULocationData* LocationData)
{
if (!IsSuccess && LocationData->Tag.MatchesTag(TargetLocation->Tag))
{
Complete();
}
}
void ULocationRestriction::OnAreaExit(ULocationData* LocationData)
{
if (IsSuccess && LocationData->Tag.MatchesTag(TargetLocation->Tag))
{
Init(Player);
}
}
@@ -1,32 +0,0 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/MissionBuilder/GoalRestriction.h"
#include "LocationRestriction.generated.h"
class ULocationData;
/**
*
*/
UCLASS()
class NAKEDDESIRE_API ULocationRestriction : public UGoalRestriction
{
GENERATED_BODY()
FDelegateHandle AreaEnterHandle;
FDelegateHandle AreaExitHandle;
UPROPERTY(EditDefaultsOnly)
ULocationData* TargetLocation;
protected:
virtual void Init(ANakedDesireCharacter* PlayerCharacter) override;
virtual void Stop() override;
virtual FText GetDescription() const override;
private:
void OnAreaEnter(ULocationData* LocationData);
void OnAreaExit(ULocationData* LocationData);
};
+1 -1
View File
@@ -11,7 +11,7 @@ public class NakedDesire : ModuleRules
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG", "CommonUI", "NavigationSystem",
"AIModule", "GameplayTags", "Slate", "SlateCore", "StructUtils"
"AIModule", "GameplayTags", "Slate", "SlateCore"
});
}
}
@@ -3,19 +3,15 @@
#include "NakedDesireCharacter.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "NakedDesire/MissionBuilder/MissionsManager.h"
#include "NakedDesire/Stats/StatsManager.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Kismet/GameplayStatics.h"
#include "Internationalization/Text.h"
#include "NakedDesire/Censorship/CensorshipComponent.h"
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Clothing/ClothingVisualsComponent.h"
#include "NakedDesire/Global/Constants.h"
#include "NakedDesire/Global/NakedDesireHUD.h"
#include "NakedDesire/Global/NakedDesireUserSettings.h"
#include "NakedDesire/Interaction/InteractionComponent.h"
#include "NakedDesire/UI/GameLayoutWidget.h"
#include "Perception/AIPerceptionStimuliSourceComponent.h"
@@ -32,7 +28,6 @@ ANakedDesireCharacter::ANakedDesireCharacter()
ClothingManager = CreateDefaultSubobject<UClothingManager>("Clothing Manager");
StatsManager = CreateDefaultSubobject<UStatsManager>("Stats Manager");
MissionsManager = CreateDefaultSubobject<UMissionsManager>("Missions Manager");
ClothingVisualsComponent = CreateDefaultSubobject<UClothingVisualsComponent>(TEXT("Clothing Visuals Component"));
@@ -22,7 +22,6 @@ class UClothingManager;
class UClothingVisualsComponent;
class UCensorshipComponent;
class UStatsManager;
class UMissionsManager;
class ANPCAIController;
class ULocationData;
@@ -87,9 +86,6 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UStatsManager* StatsManager;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UMissionsManager* MissionsManager;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UAIPerceptionStimuliSourceComponent* StimuliSourceComponent;
@@ -6,6 +6,7 @@
#include "GameFramework/SaveGame.h"
#include "NakedDesire/Global/Constants.h"
#include "ItemSaveRecord.h"
#include "NakedDesire/Commissions/CommissionTypes.h"
#include "GlobalSaveGameData.generated.h"
class UItemInstance;
@@ -42,6 +43,10 @@ public:
bool RemoveWorldItem(UItemInstance* ItemInstance);
TArray<FItemSaveRecord> GetWorldItems() const { return WorldItems; }
// Commission board state (§13). State-level only; see FCommissionSaveRecord.
TArray<FCommissionSaveRecord> GetCommissionRecords() const { return Commissions; }
void SetCommissionRecords(const TArray<FCommissionSaveRecord>& InRecords) { Commissions = InRecords; }
UPROPERTY(SaveGame)
int32 DaysPassed = 0;
@@ -66,4 +71,7 @@ private:
UPROPERTY(SaveGame)
TArray<FItemSaveRecord> WorldItems;
UPROPERTY(SaveGame)
TArray<FCommissionSaveRecord> Commissions;
};
+25 -2
View File
@@ -88,10 +88,33 @@ void UStatsManager::SetObserved(const bool bObserved, AActor* Observer)
if (!Observer)
return;
bool bChanged = false;
if (bObserved)
Observers.AddUnique(Observer);
{
if (!Observers.Contains(Observer))
{
Observers.Add(Observer);
bChanged = true;
}
}
else
Observers.Remove(Observer);
{
bChanged = Observers.Remove(Observer) > 0;
}
if (bChanged)
OnObserversChanged.Broadcast();
}
int32 UStatsManager::GetObserverCount() const
{
int32 Count = 0;
for (const TWeakObjectPtr<AActor>& Observer : Observers)
{
if (Observer.IsValid())
++Count;
}
return Count;
}
void UStatsManager::IncreaseEmbarrassment(const float Amount)
+9
View File
@@ -9,6 +9,7 @@
class UClothingManager;
class ANakedDesireCharacter;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAttributeUpdateSignature, float, CurrentValue, float, MaxValue);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FObserversChangedSignature);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
@@ -47,6 +48,14 @@ public:
// body parts that observer can actually see (GDD §7.1).
void SetObserved(bool bObserved, AActor* Observer);
// Number of NPCs currently perceiving the player (used by commission objectives, §13.4).
UFUNCTION(BlueprintPure)
int32 GetObserverCount() const;
// Fires whenever the observer set changes (an NPC gains or loses sight of the player).
UPROPERTY(BlueprintAssignable)
FObserversChangedSignature OnObserversChanged;
UFUNCTION(BlueprintCallable)
void IncreaseEmbarrassment(float Amount);
void DecreaseEmbarrassment(float Amount);