// © 2025 Naked People Team. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "NPCType.h" #include "Subsystems/WorldSubsystem.h" #include "NakedDesire/Global/TimeOfDaySubsystem.h" #include "NPCDirectorSubsystem.generated.h" class ANPC; class APawn; class UNPCDirectorConfig; /** * The GDD §17.1 NPCManager: single authority for the crowd around the player (§10.2, §19). * A WorldSubsystem (like UMissionSubsystem / UTimeOfDaySubsystem) — it needs world access plus the * day/night phase to drive density. * * Keeps a pool of NPC actors prewarmed from UNPCDirectorConfig::SpawnTable and, on a light timer, * reconciles the live population: recycles NPCs past the despawn radius and activates pooled ones at * NavMesh points in the spawn ring until the phase-appropriate target count is met. Mesh-agnostic — * it never touches what an NPC looks like, only how many exist and where. Supersedes the BP spawner. */ UCLASS() class NAKEDDESIRE_API UNPCDirectorSubsystem : public UWorldSubsystem { GENERATED_BODY() public: virtual void OnWorldBeginPlay(UWorld& InWorld) override; virtual void Deinitialize() override; private: UFUNCTION() void HandlePhaseChanged(EDayPhase NewPhase); void PrewarmPool(); void UpdatePopulation(); // timer-driven reconcile int32 CurrentTargetCount() const; ANPC* TakeFromPool(); void ReturnToPool(ANPC* NPC); ENPCType PickWeightedClass() const; bool FindSpawnPoint(const FVector& Around, FVector& OutLocation) const; TSubclassOf GetRandomNPCClass(TArray> InNPCClasses); APawn* GetPlayerPawn() const; UNPCDirectorConfig* GetConfig() const; // Inactive (hidden) NPCs ready to activate. UPROPERTY() TArray> Pool; // Currently live NPCs around the player. UPROPERTY() TArray> Active; FTimerHandle UpdateTimerHandle; EDayPhase CachedPhase = EDayPhase::Day; };