216 lines
6.0 KiB
C++
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;
|
|
} |