Added commissions system
This commit is contained in:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user