From 06fb3353b2d149924eec2b99516e0123d7b9b04c Mon Sep 17 00:00:00 2001 From: koritsa Date: Mon, 1 Jun 2026 19:12:00 +0300 Subject: [PATCH] Updated NPC plan --- PLAN.md | 79 ++++++++++++++++++++++ Source/NakedDesire/NPC/NPCAIController.cpp | 10 +++ 2 files changed, 89 insertions(+) diff --git a/PLAN.md b/PLAN.md index ede34c67..bb4357ad 100644 --- a/PLAN.md +++ b/PLAN.md @@ -432,3 +432,82 @@ Use this section for in-flight decisions, blockers, and open questions that emer - **~~README contradiction — masturbation gating.~~ RESOLVED (2026-05-30, §20 #26).** Home masturbation is always available to every player; *in-session* (public) masturbation is the Slut-path unlock. README §5.1 / §7.2 / §6.7 / §13.4 / §14.1 and §20 #26 are updated. Phase 4 / VS-2 implementation: gate only the in-session quick-action entry on Slut investment. - **Stale §5.3 Slave path text** in the README still says "(cuffs require NPC help to remove)" — contradicts the Key + minigame removal flow. Phase 6 / Phase 10 work should not implement an NPC removal path; if encountered, treat it as stale documentation. - _empty beyond this point_ + +--- + +## NPC Lively-City — Editor / Blueprint Handoff (added 2026-06-01) + +> Self-contained checklist + behavior-tree design for finishing the NPC crowd + Walker/Stalker behavior **in-editor on another machine**. The C++ foundation already exists in `Source/NakedDesire/NPC/`: `NPCType.h` (`ENPCType`), `NPCTypeDefinition` (DataAsset), `ANPC` accessors + pooling hooks, `NPCDirectorSubsystem` + `NPCDirectorConfig`, and per-observer observation weight in `StatsManager`. Everything below is editor/Blueprint work **except** the one flagged code prerequisite. + +### 0. Prerequisite — perception → blackboard wire ✅ DONE (2026-06-01) +`ANPCAIController::OnTargetPerceptionUpdate` now writes the `Player` blackboard key on each sight transition (set when sensed, cleared when lost) and resets `bHasReacted` on loss, so the BT below can branch. **The blackboard keys `Player` (Object) and `bHasReacted` (Bool) must exist in `BB_NPC` with those exact names** or the writes silently no-op. + +### 1. Data assets to author +- [ ] **`DA_Walker`** (`UNPCTypeDefinition`): `Type=Walker`, `ObservationWeight≈0.35`, `bStopsToObserve=false`, `ReactionMontage`=quick glance/head-turn. (Tune weight against the VS-1 dial.) +- [ ] **`DA_Stalker`**: `Type=Stalker`, `ObservationWeight≈1.5`, `bStopsToObserve=true`, `ObserveDurationSeconds≈5`, `ReactionMontage`=notice/stare. +- [ ] **`DA_NPCDirector`** (`UNPCDirectorConfig`): set `MaxNPCs` (≥ largest target), `TargetCountDay`/`TargetCountNight`, `SpawnRadiusMin`/`Max`, `DespawnRadius` (> Max), `UpdateInterval` (~0.5s), and a weighted `SpawnTable` (Walker class high weight, Stalker low). Tune all values. + +### 2. NPC blueprint classes +- [ ] **`BP_NPC_Walker` / `BP_NPC_Stalker`** (subclasses of `ANPC`): assign the matching `NPCTypeDefinition`, the final lightweight **skeletal mesh + standard locomotion AnimBP** (no MetaHuman, no motion matching). URO/`OnlyTickPoseWhenRendered` is already set in C++. +- [ ] Ensure `BP_NPCController` (the `ANPCAIController` BP) does **not** auto-run the BT on possess — the pool hooks (§5) own BT start/stop. + +### 3. GameInstance +- [ ] On the project GameInstance BP (`UNakedDesireGameInstance`), assign `DA_NPCDirector` to the **`NPCDirector`** property (same place `CommissionBoard` is set). + +### 4. Level +- [ ] **Remove `BPA_NPCSpawner`** from the level — `UNPCDirectorSubsystem` replaces it (world subsystem, auto-instantiated, no placement). +- [ ] Place `BP_NPCTargetLocation` wander destinations around the streets; confirm the **NavMesh** covers the playable area (the director spawns on reachable nav points). + +### 5. Pool lifecycle (events on `BP_NPC_*`) +- [ ] **`OnActivatedFromPool`** → `Run Behavior Tree (BT_NPC)`, set `bHasReacted=false`, optionally pick an initial destination. +- [ ] **`OnDeactivatedToPool`** → `Stop Logic` on the brain + `Clear Focus` (so hidden/pooled NPCs don't tick AI). + +### 6. Behavior tree (`BB_NPC` / `BT_NPC`) + +**Blackboard keys:** + +| Key | Type | Purpose | +|-----|------|---------| +| `Player` | Object | Perceived player pawn — set/cleared by the §0 code wire. Drives the react branch. | +| `TargetLocation` | Object *(or Vector)* | Current wander destination (`BP_NPCTargetLocation`). | +| `SpawnLocation` | Vector | Leash/home point (already used). | +| `bHasReacted` | Bool | Whether the notice reaction already played this sighting (prevents replay). | + +**Tree shape:** + +``` +Root +└── Selector "what should I do" + ├── [A] Sequence "stop & observe" ← Stalker-type only + │ decorators: + │ • Blackboard: Player Is Set (Observer Aborts: Both) + │ • ShouldStopToObserve == true (BP decorator, calls pawn fn) + │ children: + │ ├── BTT_PlayNoticeReaction (guard: bHasReacted == false → play montage, set true) + │ ├── BTT_StopMovement + │ ├── BTT_StareAtPlayer (SetFocus = Player, face them) [exists] + │ └── BTT_Wait (GetObserveDuration) then ClearFocus + │ + └── [B] Sequence "wander" (loop) ← default; Walkers live here + service: BTS_NoticePlayer (glance-while-walking for Walkers) + children: + ├── BTT_PickDestination → sets TargetLocation + ├── MoveTo TargetLocation [built-in] + └── BTT_Wait (random idle 1–4s) (+ optional look-around / check-phone) +``` + +**Node notes:** +- **`[A]` decorators are the entire Walker/Stalker fork.** `Player Is Set` + `ShouldStopToObserve()==true` → only Stalkers (and future Snitch/Blogger) enter; Walkers fail the second decorator and fall through to wander. Set **Observer Aborts: Both** on `Player Is Set` so a Stalker interrupts its walk the instant it sees the player and resumes wandering when the player leaves. +- **`ShouldStopToObserve` decorator** = a **Blueprint decorator** (`BTDecorator_BlueprintBase`): controlled pawn → cast `ANPC` → return `ShouldStopToObserve()`. No C++ — the accessor is already `BlueprintPure`. +- **`BTT_StareAtPlayer`** (exists): `SetFocus(Player)` / `RotateToFaceBBEntry`. Follow with **Wait** whose duration = `GetObserveDuration()` (read in a tiny BP task), or fold the whole stop+face+wait+clear into one `BTT_Observe`. +- **`BTS_NoticePlayer`** (service on `[B]`, ~0.3s): if `Player Is Set` and `bHasReacted==false` → play `NPCTypeDefinition.ReactionMontage` (glance) and set `bHasReacted=true`. This gives **Walkers a reaction without stopping** (MoveTo keeps running). Stalkers react in `[A]` instead. +- **`BTT_PickDestination`**: gather all `BP_NPCTargetLocation` actors → pick random/nearest-unvisited → set `TargetLocation`. (EQS or a random reachable nav point near `SpawnLocation` are fine alternatives.) + +**Why embarrassment needs no BT code:** the BT never touches `StatsManager`. A Stalker entering `[A]` stops and faces the player → sustains line-of-sight → the controller's perception keeps the player in its observer set → `ComputeObservedExposureRate` climbs at the Stalker's high `ObservationWeight`. A Walker passes with brief/broken LOS at low weight. Behavior and the dial reinforce each other automatically. + +### 7. Compile & verify +- [ ] Build the C++ module (after the §0 change); fix any errors. +- [ ] Walk a navmeshed greybox street: ~10–20 NPCs wander believably and varied; Stalkers stop & stare, Walkers glance & pass. +- [ ] Stand nude near a Stalker → embarrassment climbs faster than near a Walker (observation weight) and faster than fully clothed (coverage, VS-1). +- [ ] Cross the spawn ring repeatedly → no hitch (pooling), no in-view pop (off-camera spawn). +- [ ] Day→night phase change visibly changes density. diff --git a/Source/NakedDesire/NPC/NPCAIController.cpp b/Source/NakedDesire/NPC/NPCAIController.cpp index 7feaab77..8dd71684 100644 --- a/Source/NakedDesire/NPC/NPCAIController.cpp +++ b/Source/NakedDesire/NPC/NPCAIController.cpp @@ -3,6 +3,7 @@ #include "NPCAIController.h" #include "NPC.h" +#include "BehaviorTree/BlackboardComponent.h" #include "Perception/AISense_Sight.h" #include "Perception/AISenseConfig_Sight.h" #include "NakedDesire/Player/NakedDesireCharacter.h" @@ -68,4 +69,13 @@ void ANPCAIController::OnTargetPerceptionUpdate(AActor* Actor, FAIStimulus Stimu bCurrentlyObserving = bSensed; PlayerCharacter->StatsManager->SetObserved(bSensed, GetPawn(), GetObservationWeight()); + + // Surface the sighting to the behavior tree: the react/wander branch keys off "Player", and a + // fresh sighting re-arms the one-shot notice reaction (see the BT handoff in PLAN.md). + if (UBlackboardComponent* BB = GetBlackboardComponent()) + { + BB->SetValueAsObject(TEXT("Player"), bSensed ? PlayerCharacter : nullptr); + if (!bSensed) + BB->SetValueAsBool(TEXT("bHasReacted"), false); + } }