Added commision objectives

This commit is contained in:
2026-06-01 15:03:37 +03:00
parent 003d9992e2
commit f63c98d5d7
25 changed files with 626 additions and 82 deletions
+17 -9
View File
@@ -41,9 +41,9 @@ content space.
- **`ExposeBodyPart(part, duration)`** ✅ — **`UExposeBodyPartObjective`** *(implemented)*
- **`BeFullyNakedNearNPCs(count, duration)`** ✅ — **`UBeFullyNakedNearNPCsObjective`** *(implemented)*
- `WearOnlyUnderwear(duration)` ✅ — **`UWearOnlyUnderwearObjective`** *(implemented)*. Exhibitionist.
- `BeToplessOnly` / `BeBottomlessOnly(duration)` ✅ — one region bare, the other covered.
- `StayBelowCoverage(threshold, duration)` ✅ — total/region coverage under X (revealing, not nude).
- `ExposeWhileWalking(part, meters)` ✅ — keep a part revealing across N meters (per-part walk).
- `BeToplessOnly` / `BeBottomlessOnly(duration)` ✅ — **`UBareRegionObjective`** *(implemented; `EBareRegion` data value picks the bare half)*. One region bare, the other covered.
- `StayBelowCoverage(threshold, duration)` ✅ — **`UStayBelowCoverageObjective`** *(implemented; names its own threshold)*. Every region under X (revealing, not nude).
- `ExposeWhileWalking(part, meters)` ✅ — **`UExposeWhileWalkingObjective`** *(implemented)*. Keep a part revealing across N meters (per-part walk).
- `UseExposeAction(part, count)` ✅ (after VS3) — trigger the §6.3.6 flash action N times.
## Observation / NPC reactions (✅ count-based; 🔶 type-based)
@@ -65,14 +65,18 @@ content space.
- `EnterLocationNaked(tag)` ✅ · `TourLocationsNaked(tagset)` ✅ · `LingerExposed(tag, duration)`
(the last two = `BeFullyNaked` + a `ULocationConstraint`).
- `ReachLocationAwayFromClothing(tag, meters)`— arrive having left clothes ≥N m behind.
- `ReachLocationAwayFromClothing(tag, meters)`*(no new class)*`MoveDistanceFromClothing` + a `ULocationConstraint`: arrive at the location having left clothes ≥N m behind.
- `ReturnHomeNaked()` ✅-ish — end the session at the apartment with no clothing (loop-closer).
## Movement / endurance (✅)
- `WalkNakedDistance(meters)` ✅ (in §13.4) · `MoveDistanceFromClothing(meters)` ✅ (in §13.4).
- `WalkNakedDistance(meters)` ✅ (in §13.4) **`UWalkNakedDistanceObjective`** *(implemented)*. Naked travel at any gait. · `MoveDistanceFromClothing(meters)` ✅ (in §13.4) — **`UMoveDistanceFromClothingObjective`** *(implemented; polls `UDroppedClothingSubsystem` for the nearest dropped garment)*.
- `RunNakedDistance(meters)` ✅ — **`URunNakedDistanceObjective`** *(implemented)*. Distance accrues only while running + naked.
- `WalkNakedWhileObserved(meters, minObservers)` ✅.
- `WalkNakedWhileObserved(meters, minObservers)`**`UWalkNakedWhileObservedObjective`** *(implemented)*.
> All four distance objectives share **`UTravelObjectiveBase`** (`UCoverageObjectiveBase` subclass) — it owns the
> clamped distance-sampling timer; subclasses only override `DoesSampleCount()` (e.g. naked+run, part revealing,
> naked+observed). New "travel while X" objectives are a one-method subclass.
## Clothing / outfits / restraints
@@ -105,9 +109,13 @@ content space.
1. **Structural** — constraints framework + sequential steps. *(done)*
2. **All-✅ objectives** — done: `StayUnseenWhileNaked`, `GatherCrowd`, `BeObservedWhileExposed`,
`WearOnlyUnderwear`, `RunNakedDistance`, `ReachEmbarrassment`/`SustainEmbarrassment`. Location
objectives (`EnterLocationNaked`, `LingerExposed`, …) need no new class — author them as
`BeFullyNaked` / `ExposeBodyPart` + a `ULocationConstraint`.
`WearOnlyUnderwear`, `ReachEmbarrassment`/`SustainEmbarrassment`, the `UTravelObjectiveBase` distance
family (`RunNakedDistance`, `WalkNakedDistance`, `ExposeWhileWalking`, `WalkNakedWhileObserved`), and the
coverage pair `BareRegion` (topless/bottomless) + `StayBelowCoverage`. Location objectives
(`EnterLocationNaked`, `LingerExposed`, …) need no new class — author them as `BeFullyNaked` /
`ExposeBodyPart` + a `ULocationConstraint`. `MoveDistanceFromClothing` (+ its
`ReachLocationAwayFromClothing` compose) now landed on `UDroppedClothingSubsystem`. Remaining all-✅
gap: `WearOutfit` needs an outfit tag/id on items.
3. **Location system** — done: `ULocationSubsystem` + `ULocationConstraint`; the whole Location/travel
group and `WhileAtLocation` are unblocked. (Build the location *objectives* with the ✅ batch.)
4. **Per-system batches** as VS5 (NPC types), VS2 (lust/pulse), toys, restraints, and Phase 8
Binary file not shown.
+1 -1
View File
@@ -90,7 +90,7 @@ State of the C++ module as of the latest pass. File references use `Source/Naked
- **Equip / unequip / drop event** — `Clothing/ClothingManager.cpp:88-163` (slot-based, broadcasts `OnClothingEquip / Unequip / Dropped` with `UClothingItemInstance*`).
- **Censorship toggle** — compliance feature on `NakedDesireCharacter` (`BoobL/R`, `Front/BackBottom` static meshes), driven by `UNakedDesireUserSettings`.
- **AI sight + behavior tree** — `NPC/NPCAIController.cpp` runs a BT with `Player` / `TargetLocation` / `SpawnLocation` blackboard keys. `NPC/NPCSpawner.cpp` proximity-gated spawn with day / night caps.
- **Commission system — rebuilt (`Commissions/`, slice-first foundation).** Replaces the old `MissionBuilder/` Goal/Restriction model with the §13.4 vocabulary. `UCommissionObjective` is the unified typed step (owns its condition + an optional `RequiredHoldSeconds` hold timer — "expose for N s" vs "expose once" is a data value, not a class); concrete steps derive from a shared base chain — `UCommissionObjective``UObserverObjectiveBase` (reacts to the observer count) → `UCoverageObjectiveBase` (re-evaluates on equip/unequip via `ClothingManager::GetEffectiveCoverage`). Implemented steps: `BeFullyNaked`, `ExposeBodyPart`, `BeFullyNakedNearNPCs`, `StayUnseenWhileNaked`, `GatherCrowd`, `BeObservedWhileExposed`, `WearOnlyUnderwear`, `RunNakedDistance` (own distance-sampling timer), `ReachEmbarrassment`/`SustainEmbarrassment`. Location objectives (`EnterLocationNaked`, etc.) are authored as a coverage step + `ULocationConstraint` (no new class). `UCommission` is the `Offered→Accepted→Completed/Expired` state machine + `FCommissionReward` (money/XP/followers). `UMissionSubsystem` (`UWorldSubsystem`, like `TimeOfDaySubsystem`) offers a hand-authored `UCommissionBoardConfig` pool (set on `UNakedDesireGameInstance::CommissionBoard`), drives accept/abandon, **pays rewards instantly on completion** (money→save, XP→character; followers stubbed, Phase 8), **expires accepted commissions on `OnDayChanged`**, and persists state to a new `UGlobalSaveGameData::Commissions` bucket (id-keyed, state-level — objective mid-progress not preserved). Added `UStatsManager::GetObserverCount()` + `OnObserversChanged` so "near NPCs" reuses the embarrassment observer set. **Composition:** objectives gate on **constraints** (`UCommissionConstraint``UObservedConstraint`, `UDayPhaseConstraint`, `UWearingSlotConstraint`) — "do X while Y" with no new objective code; and a commission can set `bSequentialObjectives` to require its steps in array order (strip → walk → …). The full objective/constraint idea backlog with feasibility tags lives in `COMMISSIONS.md`. **Follow-ups:** no board/commission UI yet (the old forum widget isn't wired to this); `failurePenalty` is a hook only (no reputation/followers yet); procedural generation + path-filtering + the remaining §13.4 step types (`WalkNakedDistance`, `MoveDistanceFromClothing`, `PerformAction`, `BeObservedByNPCType`, `TakePhotoAtLocation`, `DeliverItemTo`) are Phase 7.
- **Commission system — rebuilt (`Commissions/`, slice-first foundation).** Replaces the old `MissionBuilder/` Goal/Restriction model with the §13.4 vocabulary. `UCommissionObjective` is the unified typed step (owns its condition + an optional `RequiredHoldSeconds` hold timer — "expose for N s" vs "expose once" is a data value, not a class); concrete steps derive from a shared base chain — `UCommissionObjective``UObserverObjectiveBase` (reacts to the observer count) → `UCoverageObjectiveBase` (re-evaluates on equip/unequip via `ClothingManager::GetEffectiveCoverage`). Implemented steps: `BeFullyNaked`, `ExposeBodyPart`, `BeFullyNakedNearNPCs`, `StayUnseenWhileNaked`, `GatherCrowd`, `BeObservedWhileExposed`, `WearOnlyUnderwear`, `BareRegion` (topless/bottomless), `StayBelowCoverage`, `ReachEmbarrassment`/`SustainEmbarrassment`, plus the `UTravelObjectiveBase` distance family — `RunNakedDistance`, `WalkNakedDistance`, `ExposeWhileWalking`, `WalkNakedWhileObserved` (shared clamped distance-sampling timer; subclasses only override `DoesSampleCount()`) — and `MoveDistanceFromClothing` (polls the new `UDroppedClothingSubsystem` for the nearest garment you left behind; `ReachLocationAwayFromClothing` is this + a `ULocationConstraint`, no new class). Location objectives (`EnterLocationNaked`, etc.) are authored as a coverage step + `ULocationConstraint` (no new class). `UCommission` is the `Offered→Accepted→Completed/Expired` state machine + `FCommissionReward` (money/XP/followers). `UMissionSubsystem` (`UWorldSubsystem`, like `TimeOfDaySubsystem`) offers a hand-authored `UCommissionBoardConfig` pool (set on `UNakedDesireGameInstance::CommissionBoard`), drives accept/abandon, **pays rewards instantly on completion** (money→save, XP→character; followers stubbed, Phase 8), **expires accepted commissions on `OnDayChanged`**, and persists state to a new `UGlobalSaveGameData::Commissions` bucket (id-keyed, state-level — objective mid-progress not preserved). Added `UStatsManager::GetObserverCount()` + `OnObserversChanged` so "near NPCs" reuses the embarrassment observer set. **Composition:** objectives gate on **constraints** (`UCommissionConstraint``UObservedConstraint`, `UDayPhaseConstraint`, `UWearingSlotConstraint`) — "do X while Y" with no new objective code; and a commission can set `bSequentialObjectives` to require its steps in array order (strip → walk → …). The full objective/constraint idea backlog with feasibility tags lives in `COMMISSIONS.md`. **Follow-ups:** no board/commission UI yet (the old forum widget isn't wired to this); `failurePenalty` is a hook only (no reputation/followers yet); procedural generation + path-filtering + the remaining §13.4 step types (`PerformAction`, `BeObservedByNPCType`, `TakePhotoAtLocation`, `DeliverItemTo`) are Phase 7.
- **Old mission framework — parked, not deleted.** `MissionBuilder/` (`Mission`/`MissionGoal`/`GoalRestriction`, `FlashGoal`/`MinTimeGoal`, the 3 restrictions, `MissionsConfig`) and the `ANakedDesireCharacter::MissionsManager` component remain on disk but dormant; remove in a cleanup pass once the new system's UI is wired and no Blueprint references the old classes.
- **Daily-mission OOB guarded** — `NakedDesireGameMode::RefreshDailyMissions` now clamps `DaysPassed` to the authored array bounds (`NakedDesireGameMode.cpp:70-71`). Still a hand-authored list (see §1.3).
- **Location system (GDD §10.4) — unified on `ULocationSubsystem`.** `Locations/LocationSubsystem` (`UWorldSubsystem`) is the single authority on which tagged locations the player occupies: `ALocationTrigger` volumes report player enter/leave (ref-counted, so overlapping boxes of one place don't churn), and it exposes `IsPlayerInLocation(tag)` (hierarchical via `FGameplayTagContainer::HasTag`), `GetCurrentLocation()`, and `OnLocationEntered/Exited`. Locations are identified by `ULocationData::Tag` and **nest** (inside `Location.City.Beach` you also match `Location.City`). `bIsApartment` is **gone**: `USessionManagerSubsystem` now subscribes to the subsystem and starts/ends the session on the native tag `TAG_Location_Apartment` (`Global/NakedDesireGameplayTags`). Commission location gating is `Commissions/Constraints/LocationConstraint`. **Content requirement:** the apartment trigger's `ULocationData` must be tagged `Location.Apartment` (or a child); each trigger box needs overlap-with-Pawn collision.
@@ -0,0 +1,51 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "DroppedClothingSubsystem.h"
#include "NakedDesire/Interactables/ItemPickup.h"
void UDroppedClothingSubsystem::RegisterPickup(AItemPickup* Pickup)
{
if (Pickup)
Pickups.AddUnique(Pickup);
}
void UDroppedClothingSubsystem::UnregisterPickup(AItemPickup* Pickup)
{
Pickups.RemoveSingleSwap(Pickup);
}
bool UDroppedClothingSubsystem::HasDroppedClothing() const
{
for (const TObjectPtr<AItemPickup>& Pickup : Pickups)
{
if (Pickup)
return true;
}
return false;
}
bool UDroppedClothingSubsystem::GetDistanceToNearestDroppedClothing(const FVector& From, float& OutDistance) const
{
float NearestSq = TNumericLimits<float>::Max();
bool bFound = false;
for (const TObjectPtr<AItemPickup>& Pickup : Pickups)
{
if (!Pickup)
continue;
const float DistSq = FVector::DistSquared(From, Pickup->GetActorLocation());
if (DistSq < NearestSq)
{
NearestSq = DistSq;
bFound = true;
}
}
if (bFound)
OutDistance = FMath::Sqrt(NearestSq);
return bFound;
}
@@ -0,0 +1,40 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "DroppedClothingSubsystem.generated.h"
class AItemPickup;
/**
* Single source of truth for which clothing is currently lying in the world (GDD §6.3.4 / §13.4).
* Every AItemPickup self-registers on BeginPlay and unregisters on EndPlay (pickup destroys the actor),
* so the tracked set always matches what is on the ground without per-query actor iteration.
*
* Backs the "move away from your clothing" objective family — distance is measured to the nearest tracked
* pickup. World-scoped and transient; the pickups themselves are persisted via the save's world items.
*/
UCLASS()
class NAKEDDESIRE_API UDroppedClothingSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
// Called by AItemPickup as it spawns / is destroyed.
void RegisterPickup(AItemPickup* Pickup);
void UnregisterPickup(AItemPickup* Pickup);
// True while at least one clothing pickup exists in the world.
UFUNCTION(BlueprintPure, Category = "Clothing")
bool HasDroppedClothing() const;
// Distance in cm from From to the nearest tracked pickup. Returns false (and leaves OutDistance
// untouched) when nothing is on the ground — callers decide what "no clothing to move from" means.
bool GetDistanceToNearestDroppedClothing(const FVector& From, float& OutDistance) const;
private:
UPROPERTY()
TArray<TObjectPtr<AItemPickup>> Pickups;
};
@@ -0,0 +1,49 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "BareRegionObjective.h"
#include "NakedDesire/Clothing/BodyPart.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.BareRegion"
bool UBareRegionObjective::IsConditionMet() const
{
switch (Region)
{
case EBareRegion::Top:
// Chest bare, both lower regions still covered.
return IsPartRevealing(EBodyPart::Boobs)
&& !IsPartRevealing(EBodyPart::Genitals)
&& !IsPartRevealing(EBodyPart::Ass);
case EBareRegion::Bottom:
// Lower half bare, chest still covered.
return IsPartRevealing(EBodyPart::Genitals)
&& IsPartRevealing(EBodyPart::Ass)
&& !IsPartRevealing(EBodyPart::Boobs);
default:
return false;
}
}
FText UBareRegionObjective::GetDescription() const
{
const bool bTimed = RequiredHoldSeconds > 0.0f;
if (Region == EBareRegion::Top)
{
return bTimed
? FText::Format(LOCTEXT("TopTimed", "Stay topless — chest bare, below covered — for {0} seconds"),
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)))
: LOCTEXT("TopInstant", "Go topless — bare your chest while staying covered below");
}
return bTimed
? FText::Format(LOCTEXT("BottomTimed", "Stay bottomless — bare below, chest covered — for {0} seconds"),
FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)))
: LOCTEXT("BottomInstant", "Go bottomless — bare below while keeping your chest covered");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,33 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "BareRegionObjective.generated.h"
// Which half is bare while the other stays covered.
UENUM(BlueprintType)
enum class EBareRegion : uint8
{
Top UMETA(DisplayName = "Topless (chest bare, below covered)"),
Bottom UMETA(DisplayName = "Bottomless (below bare, chest covered)"),
};
// BeToplessOnly / BeBottomlessOnly: one region reads as revealing while the other stays covered. A
// deliberate half-exposure objective — distinct from BeFullyNaked (all bare) and StayBelowCoverage.
UCLASS(EditInlineNew, DisplayName = "Bare One Region Only")
class NAKEDDESIRE_API UBareRegionObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override;
private:
UPROPERTY(EditDefaultsOnly)
EBareRegion Region = EBareRegion::Top;
};
@@ -47,6 +47,14 @@ bool UCoverageObjectiveBase::IsPartRevealing(EBodyPart Part) const
return Player->ClothingManager->GetEffectiveCoverage(Part) < Player->ObservationRevealThreshold;
}
float UCoverageObjectiveBase::GetEffectiveCoverage(EBodyPart Part) const
{
if (!Player || !Player->ClothingManager)
return 0.0f;
return Player->ClothingManager->GetEffectiveCoverage(Part);
}
bool UCoverageObjectiveBase::IsAnyPartRevealing() const
{
return IsPartRevealing(EBodyPart::Boobs)
@@ -27,6 +27,9 @@ protected:
// Part reads as "revealing" — below the player's observation reveal threshold.
bool IsPartRevealing(EBodyPart Part) const;
// Raw effective coverage [0,1] for a part — for objectives that name their own threshold.
float GetEffectiveCoverage(EBodyPart Part) const;
// Any of the three regions is revealing.
bool IsAnyPartRevealing() const;
@@ -0,0 +1,28 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "ExposeWhileWalkingObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.ExposeWhileWalking"
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 UExposeWhileWalkingObjective::GetDescription() const
{
return FText::Format(LOCTEXT("Walk", "Walk {0} m with your {1} exposed"),
FText::AsNumber(FMath::RoundToInt(RequiredMeters)), BodyPartText(Part));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,26 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Clothing/BodyPart.h"
#include "TravelObjectiveBase.h"
#include "ExposeWhileWalkingObjective.generated.h"
// Keep Part revealing across RequiredMeters of travel — a moving variant of ExposeBodyPart (a parade,
// not a pose). Distance accrues only while the part reads as revealing and the player is moving.
UCLASS(EditInlineNew, DisplayName = "Expose While Walking")
class NAKEDDESIRE_API UExposeWhileWalkingObjective : public UTravelObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool DoesSampleCount() const override { return IsPartRevealing(Part); }
private:
UPROPERTY(EditDefaultsOnly)
EBodyPart Part = EBodyPart::Boobs;
};
@@ -0,0 +1,84 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "MoveDistanceFromClothingObjective.h"
#include "TimerManager.h"
#include "NakedDesire/Clothing/DroppedClothingSubsystem.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.MoveDistanceFromClothing"
namespace
{
constexpr float PollIntervalSeconds = 0.2f;
}
void UMoveDistanceFromClothingObjective::OnActivate()
{
if (Player)
Player->GetWorldTimerManager().SetTimer(PollTimer, this, &UMoveDistanceFromClothingObjective::Poll, PollIntervalSeconds, true);
}
void UMoveDistanceFromClothingObjective::OnDeactivate()
{
if (Player)
Player->GetWorldTimerManager().ClearTimer(PollTimer);
PollTimer.Invalidate();
}
void UMoveDistanceFromClothingObjective::Poll()
{
NotifyConditionChanged();
}
bool UMoveDistanceFromClothingObjective::IsConditionMet() const
{
const UDroppedClothingSubsystem* Dropped = GetDroppedClothing();
if (!Dropped || !Player)
return false;
float Distance = 0.0f;
if (!Dropped->GetDistanceToNearestDroppedClothing(Player->GetActorLocation(), Distance))
return false; // nothing on the ground to move away from
return Distance >= RequiredMeters * 100.0f;
}
float UMoveDistanceFromClothingObjective::GetProgress() const
{
// While a hold timer is running (stay-away variant) defer to the base's elapsed-hold progress.
if (bSatisfied || RequiredHoldSeconds > 0.0f)
return Super::GetProgress();
const UDroppedClothingSubsystem* Dropped = GetDroppedClothing();
if (!Dropped || !Player)
return 0.0f;
float Distance = 0.0f;
if (!Dropped->GetDistanceToNearestDroppedClothing(Player->GetActorLocation(), Distance))
return 0.0f;
const float TargetCm = RequiredMeters * 100.0f;
return TargetCm > 0.0f ? FMath::Clamp(Distance / TargetCm, 0.0f, 1.0f) : 0.0f;
}
UDroppedClothingSubsystem* UMoveDistanceFromClothingObjective::GetDroppedClothing() const
{
UWorld* World = Player ? Player->GetWorld() : nullptr;
return World ? World->GetSubsystem<UDroppedClothingSubsystem>() : nullptr;
}
FText UMoveDistanceFromClothingObjective::GetDescription() const
{
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Stay {0} m from your dropped clothes for {1} seconds"),
FText::AsNumber(FMath::RoundToInt(RequiredMeters)), FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return FText::Format(LOCTEXT("Instant", "Get {0} m away from your dropped clothes"),
FText::AsNumber(FMath::RoundToInt(RequiredMeters)));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,39 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "NakedDesire/Commissions/CommissionObjective.h"
#include "MoveDistanceFromClothingObjective.generated.h"
class UDroppedClothingSubsystem;
// §13.4 MoveDistanceFromClothing(meters): reach RequiredMeters of separation from the nearest clothing
// you left on the ground (UDroppedClothingSubsystem). Distance has no event, so the objective polls.
// Never auto-satisfies when nothing has been dropped — there is nothing to move away from.
//
// ReachLocationAwayFromClothing(tag, meters) needs no new class: author this objective with a
// ULocationConstraint, and the base ANDs the location gate into the satisfaction check.
UCLASS(EditInlineNew, DisplayName = "Move Distance From Clothing")
class NAKEDDESIRE_API UMoveDistanceFromClothingObjective : public UCommissionObjective
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
virtual float GetProgress() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
virtual bool IsConditionMet() const override;
private:
void Poll();
UDroppedClothingSubsystem* GetDroppedClothing() const;
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
float RequiredMeters = 50.0f;
FTimerHandle PollTimer;
};
@@ -3,65 +3,14 @@
#include "RunNakedDistanceObjective.h"
#include "TimerManager.h"
#include "NakedDesire/Global/Gait.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.RunNakedDistance"
namespace
bool URunNakedDistanceObjective::DoesSampleCount() const
{
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;
return IsFullyNaked() && Player && Player->GetGait() == EGait::Run;
}
FText URunNakedDistanceObjective::GetDescription() const
@@ -3,31 +3,19 @@
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "TravelObjectiveBase.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.
// condition (and any constraints) hold; the base clamps per-sample movement so a teleport can't satisfy it.
UCLASS(EditInlineNew, DisplayName = "Run Naked Distance")
class NAKEDDESIRE_API URunNakedDistanceObjective : public UCoverageObjectiveBase
class NAKEDDESIRE_API URunNakedDistanceObjective : public UTravelObjectiveBase
{
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;
virtual bool DoesSampleCount() const override;
};
@@ -0,0 +1,29 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "StayBelowCoverageObjective.h"
#include "NakedDesire/Clothing/BodyPart.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.StayBelowCoverage"
bool UStayBelowCoverageObjective::IsConditionMet() const
{
return GetEffectiveCoverage(EBodyPart::Boobs) <= CoverageThreshold
&& GetEffectiveCoverage(EBodyPart::Ass) <= CoverageThreshold
&& GetEffectiveCoverage(EBodyPart::Genitals) <= CoverageThreshold;
}
FText UStayBelowCoverageObjective::GetDescription() const
{
if (RequiredHoldSeconds > 0.0f)
{
return FText::Format(LOCTEXT("Timed", "Keep every part under {0} covered for {1} seconds"),
FText::AsPercent(CoverageThreshold), FText::AsNumber(FMath::RoundToInt(RequiredHoldSeconds)));
}
return FText::Format(LOCTEXT("Instant", "Bare yourself until every part is under {0} covered"),
FText::AsPercent(CoverageThreshold));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,26 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "StayBelowCoverageObjective.generated.h"
// StayBelowCoverage(threshold, duration): every exposable region must read at or below CoverageThreshold
// — revealing all over without being fully nude. Names its own threshold rather than reusing the player's
// reveal cutoff, so it can ask for "barely dressed" independent of the observation tuning.
UCLASS(EditInlineNew, DisplayName = "Stay Below Coverage")
class NAKEDDESIRE_API UStayBelowCoverageObjective : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool IsConditionMet() const override;
private:
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0, ClampMax = 1))
float CoverageThreshold = 0.5f;
};
@@ -0,0 +1,61 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "TravelObjectiveBase.h"
#include "TimerManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
namespace
{
constexpr float SampleIntervalSeconds = 0.2f;
constexpr float MaxStepCm = 1000.0f; // ignore single-sample jumps larger than this (teleports / streaming)
}
void UTravelObjectiveBase::OnActivate()
{
Super::OnActivate();
AccumulatedCm = 0.0f;
if (Player)
{
LastLocation = Player->GetActorLocation();
Player->GetWorldTimerManager().SetTimer(SampleTimer, this, &UTravelObjectiveBase::SampleDistance, SampleIntervalSeconds, true);
}
}
void UTravelObjectiveBase::OnDeactivate()
{
if (Player)
Player->GetWorldTimerManager().ClearTimer(SampleTimer);
SampleTimer.Invalidate();
Super::OnDeactivate();
}
void UTravelObjectiveBase::SampleDistance()
{
if (bSatisfied || !Player)
return;
const FVector Now = Player->GetActorLocation();
const float Delta = FVector::Dist(Now, LastLocation);
LastLocation = Now;
if (DoesSampleCount() && AreConstraintsMet())
{
AccumulatedCm += FMath::Min(Delta, MaxStepCm);
if (AccumulatedCm >= RequiredMeters * 100.0f)
MarkSatisfied();
}
}
float UTravelObjectiveBase::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;
}
@@ -0,0 +1,36 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "CoverageObjectiveBase.h"
#include "TravelObjectiveBase.generated.h"
// Shared base for "cover RequiredMeters while <condition> holds" objectives. Owns the distance-sampling
// timer; per-sample movement is clamped so a teleport / streaming jump can't satisfy it. Subclasses only
// declare what counts via DoesSampleCount() (e.g. naked + running, or a part revealing while walking).
UCLASS(Abstract)
class NAKEDDESIRE_API UTravelObjectiveBase : public UCoverageObjectiveBase
{
GENERATED_BODY()
public:
virtual float GetProgress() const override;
protected:
virtual void OnActivate() override;
virtual void OnDeactivate() override;
// Whether the latest movement sample should accrue toward the distance (constraints are ANDed on top).
virtual bool DoesSampleCount() const { return false; }
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
float RequiredMeters = 50.0f;
private:
void SampleDistance();
float AccumulatedCm = 0.0f;
FVector LastLocation = FVector::ZeroVector;
FTimerHandle SampleTimer;
};
@@ -0,0 +1,13 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "WalkNakedDistanceObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.WalkNakedDistance"
FText UWalkNakedDistanceObjective::GetDescription() const
{
return FText::Format(LOCTEXT("Walk", "Walk {0} m naked"), FText::AsNumber(FMath::RoundToInt(RequiredMeters)));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,21 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "TravelObjectiveBase.h"
#include "WalkNakedDistanceObjective.generated.h"
// §13.4 WalkNakedDistance(meters): cover RequiredMeters on foot while fully naked, at any gait. The
// general "travel naked" objective — distinct from RunNakedDistance, which additionally requires running.
UCLASS(EditInlineNew, DisplayName = "Walk Naked Distance")
class NAKEDDESIRE_API UWalkNakedDistanceObjective : public UTravelObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool DoesSampleCount() const override { return IsFullyNaked(); }
};
@@ -0,0 +1,14 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "WalkNakedWhileObservedObjective.h"
#define LOCTEXT_NAMESPACE "Commissions.Objectives.WalkNakedWhileObserved"
FText UWalkNakedWhileObservedObjective::GetDescription() const
{
return FText::Format(LOCTEXT("Walk", "Walk {0} m naked with at least {1} watching"),
FText::AsNumber(FMath::RoundToInt(RequiredMeters)), FText::AsNumber(MinObservers));
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,25 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "TravelObjectiveBase.h"
#include "WalkNakedWhileObservedObjective.generated.h"
// Cover RequiredMeters while fully naked AND with at least MinObservers watching — a "naked parade past
// a crowd" objective. Distance only accrues while both hold (lose your audience and progress stalls).
UCLASS(EditInlineNew, DisplayName = "Walk Naked While Observed")
class NAKEDDESIRE_API UWalkNakedWhileObservedObjective : public UTravelObjectiveBase
{
GENERATED_BODY()
public:
virtual FText GetDescription() const override;
protected:
virtual bool DoesSampleCount() const override { return IsFullyNaked() && GetObserverCount() >= MinObservers; }
private:
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 1))
int32 MinObservers = 1;
};
@@ -6,6 +6,7 @@
#include "Components/WidgetComponent.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
#include "NakedDesire/Clothing/DroppedClothingSubsystem.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Clothing/ClothingManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
@@ -65,6 +66,17 @@ void AItemPickup::BeginPlay()
Super::BeginPlay();
InteractionHint->SetVisibility(false);
if (UDroppedClothingSubsystem* Dropped = GetWorld()->GetSubsystem<UDroppedClothingSubsystem>())
Dropped->RegisterPickup(this);
}
void AItemPickup::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (UDroppedClothingSubsystem* Dropped = GetWorld()->GetSubsystem<UDroppedClothingSubsystem>())
Dropped->UnregisterPickup(this);
Super::EndPlay(EndPlayReason);
}
void AItemPickup::SetItem(UClothingItemInstance* InItem)
@@ -30,6 +30,7 @@ public:
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
UPROPERTY()