// © 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 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 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; };