// © 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()) Time->OnDayChanged.AddUniqueDynamic(this, &UMissionSubsystem::HandleDayChanged); BuildBoard(); RestoreFromSave(); OnBoardChanged.Broadcast(); } void UMissionSubsystem::Deinitialize() { if (const UWorld* World = GetWorld()) { if (UTimeOfDaySubsystem* Time = World->GetSubsystem()) 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(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 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(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); } UGlobalSaveGameData* UMissionSubsystem::GetSave() const { UGameInstance* GameInstance = GetWorld() ? GetWorld()->GetGameInstance() : nullptr; USaveSubsystem* SaveSubsystem = GameInstance ? GameInstance->GetSubsystem() : nullptr; return SaveSubsystem ? SaveSubsystem->GetCurrentSave() : nullptr; } UCommissionBoardConfig* UMissionSubsystem::GetBoardConfig() const { const UNakedDesireGameInstance* GameInstance = Cast(GetWorld() ? GetWorld()->GetGameInstance() : nullptr); return GameInstance ? GameInstance->CommissionBoard : nullptr; }