Files
Naked-Desire/Source/NakedDesire/Commissions/MissionSubsystem.cpp
T
2026-06-03 15:17:02 +03:00

216 lines
6.0 KiB
C++

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