Enhance narrative systems with detailed implementation guides and data-driven structures

- Updated BPC_NarrativeStateSystem with a comprehensive manual implementation guide, including class setup, variable initialization, and function breakdowns.
- Expanded BPC_ObjectiveSystem documentation to include a manual implementation guide and detailed function descriptions.
- Added a manual implementation guide for BPC_DialoguePlaybackSystem, outlining class setup and function nodes.
- Introduced a manual implementation guide for BPC_DialogueChoiceSystem, detailing choice presentation and selection processes.
- Enhanced BPC_BranchingConsequenceSystem documentation with a manual implementation guide for consequence evaluation.
- Updated BPC_TrialScenarioSystem with a manual implementation guide for scenario management.
- Expanded BPC_LoreUnlockSystem documentation to include a manual implementation guide for lore entry management.
- Added a manual implementation guide for BP_NarrativeTriggerVolume, detailing trigger volume setup and action execution.
- Enhanced BPC_EndingAccumulator documentation with a manual implementation guide for ending evaluation.
- Updated BPC_HitReactionSystem with a manual implementation guide for hit reaction management.
- Added a manual implementation guide for BPC_RecoilSystem, detailing recoil application and recovery processes.
- Introduced DT_ProjectTags.csv to define gameplay tags for various systems, enhancing data-driven design capabilities.
This commit is contained in:
Lefteris Notas
2026-05-19 18:48:37 +03:00
parent eeb1bf82c9
commit bec6cb715e
12 changed files with 745 additions and 11 deletions

View File

@@ -0,0 +1,65 @@
Name,Tag,DevComment
Framework_Player_State_Alive,Framework.Player.State.Alive,
Framework_Player_State_Dead,Framework.Player.State.Dead,
Framework_Player_State_Hidden,Framework.Player.State.Hidden,
Framework_Player_State_Interacting,Framework.Player.State.Interacting,
Framework_Player_Stress_Low,Framework.Player.Stress.Low,
Framework_Player_Stress_Mid,Framework.Player.Stress.Mid,
Framework_Player_Stress_High,Framework.Player.Stress.High,
Framework_Player_Stress_Critical,Framework.Player.Stress.Critical,
Framework_Player_Posture_Standing,Framework.Player.Posture.Standing,
Framework_Player_Posture_Crouching,Framework.Player.Posture.Crouching,
Framework_Player_Posture_Prone,Framework.Player.Posture.Prone,
Framework_Player_Posture_Vaulting,Framework.Player.Posture.Vaulting,
Framework_Interaction_Type_Pickup,Framework.Interaction.Type.Pickup,
Framework_Interaction_Type_Door,Framework.Interaction.Type.Door,
Framework_Interaction_Type_Drawer,Framework.Interaction.Type.Drawer,
Framework_Interaction_Type_Container,Framework.Interaction.Type.Container,
Framework_Interaction_Type_Inspect,Framework.Interaction.Type.Inspect,
Framework_Interaction_Type_Climb,Framework.Interaction.Type.Climb,
Framework_Interaction_Type_Hide,Framework.Interaction.Type.Hide,
Framework_Interaction_Type_Use,Framework.Interaction.Type.Use,
Framework_Interaction_Type_Combine,Framework.Interaction.Type.Combine,
Framework_Interaction_Context_Requires_Key,Framework.Interaction.Context.Requires.Key,
Framework_Interaction_Context_Requires_Item,Framework.Interaction.Context.Requires.Item,
Framework_Interaction_Context_Locked,Framework.Interaction.Context.Locked,
Framework_Interaction_Context_Disabled,Framework.Interaction.Context.Disabled,
Framework_Item_Type_Weapon,Framework.Item.Type.Weapon,
Framework_Item_Type_Consumable,Framework.Item.Type.Consumable,
Framework_Item_Type_KeyItem,Framework.Item.Type.KeyItem,
Framework_Item_Type_Document,Framework.Item.Type.Document,
Framework_Item_Type_Collectible,Framework.Item.Type.Collectible,
Framework_Item_Type_Ammo,Framework.Item.Type.Ammo,
Framework_Item_Type_Tool,Framework.Item.Type.Tool,
Framework_Item_Slot_PrimaryWeapon,Framework.Item.Slot.PrimaryWeapon,
Framework_Item_Slot_SecondaryWeapon,Framework.Item.Slot.SecondaryWeapon,
Framework_Item_Slot_Flashlight,Framework.Item.Slot.Flashlight,
Framework_Item_Slot_Shield,Framework.Item.Slot.Shield,
Framework_Item_Slot_Active,Framework.Item.Slot.Active,
Game_Narrative_Flag,Game.Narrative.Flag,
Game_Narrative_Phase,Game.Narrative.Phase,
Game_Narrative_Choice,Game.Narrative.Choice,
Game_Narrative_Ending,Game.Narrative.Ending,
Game_Narrative_Flag_PrologueComplete,Game.Narrative.Flag.PrologueComplete,
Framework_Objective_Status_Active,Framework.Objective.Status.Active,
Framework_Objective_Status_Complete,Framework.Objective.Status.Complete,
Framework_Objective_Status_Failed,Framework.Objective.Status.Failed,
Framework_Objective_Status_Hidden,Framework.Objective.Status.Hidden,
Framework_AI_Alert_None,Framework.AI.Alert.None,
Framework_AI_Alert_Suspicious,Framework.AI.Alert.Suspicious,
Framework_AI_Alert_Alerted,Framework.AI.Alert.Alerted,
Framework_AI_Alert_Engaged,Framework.AI.Alert.Engaged,
Framework_AI_Archetype_Patrol,Framework.AI.Archetype.Patrol,
Framework_AI_Archetype_Ambush,Framework.AI.Archetype.Ambush,
Framework_AI_Archetype_Stalker,Framework.AI.Archetype.Stalker,
Framework_AI_Archetype_Passive,Framework.AI.Archetype.Passive,
Framework_Save_Type_Checkpoint,Framework.Save.Type.Checkpoint,
Framework_Save_Type_HardSave,Framework.Save.Type.HardSave,
Framework_Save_Type_AutoSave,Framework.Save.Type.AutoSave,
Game_Achievement,Game.Achievement,
Game_Achievement_FirstBlood,Game.Achievement.FirstBlood,
Game_Environment_Atmosphere,Game.Environment.Atmosphere,
Game_Environment_Atmosphere_Eerie,Game.Environment.Atmosphere.Eerie,
Game_Environment_Scare,Game.Environment.Scare,
Game_Environment_Scare_MirrorJump,Game.Environment.Scare.MirrorJump,
Framework_DeathSpace_Active,Framework.DeathSpace.Active,
1 Name Tag DevComment
2 Framework_Player_State_Alive Framework.Player.State.Alive
3 Framework_Player_State_Dead Framework.Player.State.Dead
4 Framework_Player_State_Hidden Framework.Player.State.Hidden
5 Framework_Player_State_Interacting Framework.Player.State.Interacting
6 Framework_Player_Stress_Low Framework.Player.Stress.Low
7 Framework_Player_Stress_Mid Framework.Player.Stress.Mid
8 Framework_Player_Stress_High Framework.Player.Stress.High
9 Framework_Player_Stress_Critical Framework.Player.Stress.Critical
10 Framework_Player_Posture_Standing Framework.Player.Posture.Standing
11 Framework_Player_Posture_Crouching Framework.Player.Posture.Crouching
12 Framework_Player_Posture_Prone Framework.Player.Posture.Prone
13 Framework_Player_Posture_Vaulting Framework.Player.Posture.Vaulting
14 Framework_Interaction_Type_Pickup Framework.Interaction.Type.Pickup
15 Framework_Interaction_Type_Door Framework.Interaction.Type.Door
16 Framework_Interaction_Type_Drawer Framework.Interaction.Type.Drawer
17 Framework_Interaction_Type_Container Framework.Interaction.Type.Container
18 Framework_Interaction_Type_Inspect Framework.Interaction.Type.Inspect
19 Framework_Interaction_Type_Climb Framework.Interaction.Type.Climb
20 Framework_Interaction_Type_Hide Framework.Interaction.Type.Hide
21 Framework_Interaction_Type_Use Framework.Interaction.Type.Use
22 Framework_Interaction_Type_Combine Framework.Interaction.Type.Combine
23 Framework_Interaction_Context_Requires_Key Framework.Interaction.Context.Requires.Key
24 Framework_Interaction_Context_Requires_Item Framework.Interaction.Context.Requires.Item
25 Framework_Interaction_Context_Locked Framework.Interaction.Context.Locked
26 Framework_Interaction_Context_Disabled Framework.Interaction.Context.Disabled
27 Framework_Item_Type_Weapon Framework.Item.Type.Weapon
28 Framework_Item_Type_Consumable Framework.Item.Type.Consumable
29 Framework_Item_Type_KeyItem Framework.Item.Type.KeyItem
30 Framework_Item_Type_Document Framework.Item.Type.Document
31 Framework_Item_Type_Collectible Framework.Item.Type.Collectible
32 Framework_Item_Type_Ammo Framework.Item.Type.Ammo
33 Framework_Item_Type_Tool Framework.Item.Type.Tool
34 Framework_Item_Slot_PrimaryWeapon Framework.Item.Slot.PrimaryWeapon
35 Framework_Item_Slot_SecondaryWeapon Framework.Item.Slot.SecondaryWeapon
36 Framework_Item_Slot_Flashlight Framework.Item.Slot.Flashlight
37 Framework_Item_Slot_Shield Framework.Item.Slot.Shield
38 Framework_Item_Slot_Active Framework.Item.Slot.Active
39 Game_Narrative_Flag Game.Narrative.Flag
40 Game_Narrative_Phase Game.Narrative.Phase
41 Game_Narrative_Choice Game.Narrative.Choice
42 Game_Narrative_Ending Game.Narrative.Ending
43 Game_Narrative_Flag_PrologueComplete Game.Narrative.Flag.PrologueComplete
44 Framework_Objective_Status_Active Framework.Objective.Status.Active
45 Framework_Objective_Status_Complete Framework.Objective.Status.Complete
46 Framework_Objective_Status_Failed Framework.Objective.Status.Failed
47 Framework_Objective_Status_Hidden Framework.Objective.Status.Hidden
48 Framework_AI_Alert_None Framework.AI.Alert.None
49 Framework_AI_Alert_Suspicious Framework.AI.Alert.Suspicious
50 Framework_AI_Alert_Alerted Framework.AI.Alert.Alerted
51 Framework_AI_Alert_Engaged Framework.AI.Alert.Engaged
52 Framework_AI_Archetype_Patrol Framework.AI.Archetype.Patrol
53 Framework_AI_Archetype_Ambush Framework.AI.Archetype.Ambush
54 Framework_AI_Archetype_Stalker Framework.AI.Archetype.Stalker
55 Framework_AI_Archetype_Passive Framework.AI.Archetype.Passive
56 Framework_Save_Type_Checkpoint Framework.Save.Type.Checkpoint
57 Framework_Save_Type_HardSave Framework.Save.Type.HardSave
58 Framework_Save_Type_AutoSave Framework.Save.Type.AutoSave
59 Game_Achievement Game.Achievement
60 Game_Achievement_FirstBlood Game.Achievement.FirstBlood
61 Game_Environment_Atmosphere Game.Environment.Atmosphere
62 Game_Environment_Atmosphere_Eerie Game.Environment.Atmosphere.Eerie
63 Game_Environment_Scare Game.Environment.Scare
64 Game_Environment_Scare_MirrorJump Game.Environment.Scare.MirrorJump
65 Framework_DeathSpace_Active Framework.DeathSpace.Active

View File

@@ -111,4 +111,80 @@ None required. Flat Tag maps suffice.
| [`SS_SaveManager`](../05-save/28_SS_SaveManager.md) | [`I_Persistable`](../01-core/29_I_Persistable.md) | Flag persistence across sessions | | [`SS_SaveManager`](../05-save/28_SS_SaveManager.md) | [`I_Persistable`](../01-core/29_I_Persistable.md) | Flag persistence across sessions |
### Reuse Notes ### Reuse Notes
The entire system is tag-driven and fully generic. The `Narrative.Flag.*` and `Narrative.Phase.*` namespaces are empty by default — fill them per project with game-specific flags. Add new numeric value types (reputation, corruption, etc.) by simply calling `SetValue` with the appropriate tag. No code/blueprint changes needed. The entire system is tag-driven and fully generic. The `Narrative.Flag.*` and `Narrative.Phase.*` namespaces are empty by default — fill them per project with game-specific flags. Add new numeric value types (reputation, corruption, etc.) by simply calling `SetValue` with the appropriate tag. No code/blueprint changes needed.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_NarrativeStateSystem`
2. Add to Player Character or GameState (if shared)
3. Implement Interface: `I_Persistable`
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set NarrativeFlags = empty Map<GameplayTag, Boolean>
├─ Set NarrativeValues = empty Map<GameplayTag, Float>
├─ Set NarrativeHistory = empty Array<GameplayTag>
├─ Set MaxHistorySize = 500
└─ If implementing I_Persistable: register with SS_SaveManager
```
### Function Node-by-Node
#### `SetFlag(Tag: GameplayTag, Value: Boolean)` → `void`
```
Step 1: OldValue = NarrativeFlags.Find(Tag) or false
Step 2: NarrativeFlags.Add(Tag, Value) ← Map Add (overwrites if exists)
Step 3: If Value == true AND OldValue == false:
NarrativeHistory.Add(Tag)
If NarrativeHistory.Length > MaxHistorySize: Remove oldest entry
Step 4: Fire OnFlagChanged(Tag, Value)
Step 5: Notify I_Persistable: Mark dirty for save
```
**Nodes:** `Map Find`, `Map Add`, `Array Add`, `Array Length`, `Remove Index 0`
#### `SetValue(Tag, Value: Float)` → `void`
```
Step 1: NarrativeValues.Add(Tag, Value)
Step 2: Fire OnValueChanged(Tag, Value)
```
#### `GetFlag(Tag)` → `Boolean` *(Pure)*
```
NarrativeFlags.Find(Tag) → if found: return value, else: return false
```
#### `HasAllFlags(Tags: Array<GameplayTag>)` → `Boolean`
```
ForEach Tags:
If GetFlag(ArrayElement) == false → Return false
Return true
```
#### `CollectState()` → `S_WorldObjectState` *(I_Persistable)*
```
Step 1: Create S_WorldObjectState
Step 2: Serialize NarrativeFlags: ForEach → store (Tag, Value) as string pairs in CustomData
Step 3: Serialize NarrativeValues similarly
Step 4: Return struct
```
#### `RestoreState(Data: S_WorldObjectState)` *(I_Persistable)*
```
Step 1: Clear NarrativeFlags, NarrativeValues, NarrativeHistory
Step 2: Parse CustomData → ForEach entry → NarrativeFlags.Add(Tag, BoolValue)
Step 3: Parse values → NarrativeValues.Add(Tag, FloatValue)
```
### Build Checklist
- [ ] Create BPC_NarrativeStateSystem, add to Player Character
- [ ] Define variables: NarrativeFlags (Map), NarrativeValues (Map), NarrativeHistory (Array), MaxHistorySize
- [ ] Implement SetFlag/GetFlag with map operations
- [ ] Implement SetValue/GetValue for numeric values
- [ ] Implement HasAllFlags/HasAnyFlags for batch queries
- [ ] Implement CollectState/RestoreState for I_Persistable
- [ ] Create OnFlagChanged/OnValueChanged event dispatchers
- [ ] Test: call SetFlag → verify OnFlagChanged fires → other systems react

View File

@@ -114,4 +114,75 @@ Tracks active, completed, and failed objectives. Supports main objectives, sub-o
| [`SS_SaveManager`](../05-save/28_SS_SaveManager.md) | [`I_Persistable`](../01-core/29_I_Persistable.md) | Objective state persistence | | [`SS_SaveManager`](../05-save/28_SS_SaveManager.md) | [`I_Persistable`](../01-core/29_I_Persistable.md) | Objective state persistence |
### Reuse Notes ### Reuse Notes
Define objectives via `DA_ObjectiveData` data assets (one per objective) that specify display text, dependencies, associated narrative flag, and optional/hidden flags. The objective system reads from these assets at level start. Adding a new objective type (like timed objectives) requires adding an `S_TimedObjective` struct extension and a timer function — no system redesign needed. Define objectives via `DA_ObjectiveData` data assets (one per objective) that specify display text, dependencies, associated narrative flag, and optional/hidden flags. The objective system reads from these assets at level start. Adding a new objective type (like timed objectives) requires adding an `S_TimedObjective` struct extension and a timer function — no system redesign needed.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_ObjectiveSystem`
2. Add to Player Character
3. Define struct `S_ObjectiveState`: ObjectiveTag, Status (E_ObjectiveStatus), DisplayText (Text), Description (Text), bIsHidden (Boolean), bIsOptional (Boolean), Dependencies (Array<GameplayTag>)
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set AllObjectives = empty Map<GameplayTag, S_ObjectiveState>
├─ Set ActiveObjectiveTags, CompletedObjectiveTags, FailedObjectiveTags = empty arrays
└─ Get Owner → Cache BPC_NarrativeStateSystem
```
### Function Node-by-Node
#### `RegisterObjectiveFromDataAsset(ObjTag, DataAsset)` → `void`
```
Step 1: Read DA_ObjectiveData fields
Step 2: Create S_ObjectiveState struct:
ObjectiveTag = ObjTag, Status = Inactive
DisplayText = DataAsset.ObjectiveText
Dependencies = DataAsset.PrerequisiteFlags
bIsHidden = (DataAsset.ObjectiveCategory == Hidden)
bIsOptional = DataAsset.bIsOptional
Step 3: AllObjectives.Add(ObjTag, newState)
```
#### `ActivateObjective(ObjTag)` → `void`
```
Step 1: State = AllObjectives.Find(ObjTag) — if not found, return
Step 2: If State.bIsHidden: return (hidden until revealed)
Step 3: Call CheckDependenciesMet(ObjTag) → if not met: set Status=Inactive, return
Step 4: State.Status = Active
Step 5: ActiveObjectiveTags.Add Unique(ObjTag)
Step 6: Fire OnObjectiveActivated(ObjTag, DisplayData)
Step 7: Update GS_CoreGameState.ActiveObjectiveTags
```
#### `CompleteObjective(ObjTag)` → `void`
```
Step 1: Validate Status == Active → if not, return
Step 2: State.Status = Complete
Step 3: Move: ActiveObjectiveTags.Remove(ObjTag) → CompletedObjectiveTags.Add(ObjTag)
Step 4: Fire OnObjectiveCompleted(ObjTag)
Step 5: Set narrative flag: BPC_NarrativeStateSystem.SetFlag(ObjTag, true)
Step 6: Check dependent objectives: ForEach AllObjectives → if Deps include ObjTag → ActivateObjective
```
#### `GetActiveObjectives()` → `Array<S_ObjectiveDisplayData>`
```
Create results array → ForEach ActiveObjectiveTags:
State = AllObjectives.Find(tag)
Create S_ObjectiveDisplayData(Tag, State.DisplayText, false, State.bIsOptional)
Add to results
Return results
```
### Build Checklist
- [ ] Create BPC_ObjectiveSystem, add to Player Character
- [ ] Define E_ObjectiveStatus enum and S_ObjectiveState struct
- [ ] Implement RegisterObjectiveFromDataAsset
- [ ] Implement ActivateObjective with dependency check
- [ ] Implement CompleteObjective with cascading activation
- [ ] Implement GetActiveObjectives for UI binding
- [ ] Sync to GS_CoreGameState.ActiveObjectiveTags
- [ ] Test: register objective → activate → complete → dependent activates

View File

@@ -119,4 +119,75 @@ Manages the playback of dialogue sequences: line queuing, timing, subtitle routi
| [`GI_GameFramework`](../01-core/04_GI_GameFramework.md) | Direct | Set game phase during dialogue | | [`GI_GameFramework`](../01-core/04_GI_GameFramework.md) | Direct | Set game phase during dialogue |
### Reuse Notes ### Reuse Notes
`DA_DialogueSequence` data assets hold all content — add new sequences per project without touching this system. Voice audio is optional: sequences work without audio (text-only dialogue). Lip-sync data is per-line and can use audio-driven or procedural lip sync. `DA_DialogueSequence` data assets hold all content — add new sequences per project without touching this system. Voice audio is optional: sequences work without audio (text-only dialogue). Lip-sync data is per-line and can use audio-driven or procedural lip sync.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_DialoguePlaybackSystem`
2. Add to Player Character
3. Define struct `S_DialogueLine`: SpeakerTag, LineText (Text), VoiceAudio (SoundBase), Duration (Float), FlagToSetOnComplete (GameplayTag), bIsChoicePoint (Boolean)
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set bIsPlaying = false, bIsPaused = false
├─ Set LineQueue = empty Array<S_DialogueLine>
└─ Cache: BPC_NarrativeStateSystem, SS_AudioManager (via Get Game Instance)
```
### Function Node-by-Node
#### `PlaySequence(Sequence: DA_DialogueSequence)` → `void`
```
Step 1: If bIsPlaying → QueueSequence (or return error)
Step 2: Validate RequiredFlags: Sequence.RequiredFlags → HasAllFlags → if false, return
Step 3: Set bIsPlaying = true
Step 4: Load LineQueue from Sequence.Lines array (copy all lines)
Step 5: Set CurrentLineIndex = 0
Step 6: Fire OnDialogueStarted(Sequence.SequenceTag)
Step 7: Call PlayNextLine()
```
#### `PlayNextLine()` → `void`
```
Step 1: If CurrentLineIndex >= LineQueue.Length:
Fire OnSequenceCompleted → Set bIsPlaying=false → Return
Step 2: CurrentLine = LineQueue[CurrentLineIndex]
Step 3: If CurrentLine.bIsChoicePoint:
Fire OnChoicePointReached → hand to BPC_DialogueChoiceSystem → pause here
Return (resume when choice returns NextSequenceTag)
Step 4: Fire OnLineStarted(CurrentLine)
Step 5: If CurrentLine.VoiceAudio valid:
SS_AudioManager.PlayDialogue(CurrentLine.VoiceAudio)
Step 6: Show subtitle: Fire dispatcher → WBP_SubtitleDisplay.Show(CurrentLine.LineText, SpeakerTag)
Step 7: Set Timer (CurrentLine.Duration) → On timer: Call OnLineCompleted()
```
#### `OnLineCompleted()` → `void`
```
Step 1: If CurrentLine.FlagToSetOnComplete valid:
BPC_NarrativeStateSystem.SetFlag(FlagToSetOnComplete, true)
Step 2: Fire OnLineCompleted(CurrentLine)
Step 3: Increment CurrentLineIndex
Step 4: Call PlayNextLine()
```
#### `SkipCurrentLine()` → `void`
```
Step 1: Clear LineTimer
Step 2: Stop VoiceAudio if playing
Step 3: Fire OnLineCompleted (skip to next line)
```
### Build Checklist
- [ ] Create BPC_DialoguePlaybackSystem, add to Player Character
- [ ] Define S_DialogueLine struct and DA_DialogueSequence Data Asset
- [ ] Implement PlaySequence with flag validation
- [ ] Implement PlayNextLine with line queue + timer + audio + subtitle
- [ ] Implement choice point handoff to DialogueChoiceSystem
- [ ] Implement SkipCurrentLine/SkipSequence
- [ ] Wire subtitles to WBP_SubtitleDisplay via dispatcher
- [ ] Test: trigger dialogue → lines play sequentially → subtitles show → audio plays

View File

@@ -104,4 +104,77 @@ Presents branching dialogue choices to the player and routes the selected respon
| [`BPC_BranchingConsequenceSystem`](42_BPC_BranchingConsequenceSystem.md) | Dispatcher | Choice flag changes trigger consequence evaluation | | [`BPC_BranchingConsequenceSystem`](42_BPC_BranchingConsequenceSystem.md) | Dispatcher | Choice flag changes trigger consequence evaluation |
### Reuse Notes ### Reuse Notes
Choice filtering by RequiredFlagTag allows context-sensitive dialogue without branching logic in the system. Choices can be hidden (e.g., secret dialogue options only appear if player has a specific lore unlock). The system handles all choice patterns: timed, untimed, locked, hidden, and priority-sorted. Choice filtering by RequiredFlagTag allows context-sensitive dialogue without branching logic in the system. Choices can be hidden (e.g., secret dialogue options only appear if player has a specific lore unlock). The system handles all choice patterns: timed, untimed, locked, hidden, and priority-sorted.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_DialogueChoiceSystem`
2. Add to Player Character
3. Define struct `S_DialogueChoice`: ChoiceText (Text), ResultFlagTag (GameplayTag), NextSequenceTag (GameplayTag), RequiredFlagTag (GameplayTag), Priority (Integer)
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set CurrentChoices = empty array
├─ Set bChoiceActive = false
├─ Set ChoiceTimeLimit = 0.0
└─ Cache: BPC_NarrativeStateSystem, BPC_DialoguePlaybackSystem
```
### Function Node-by-Node
#### `PresentChoices(Choices: Array<S_DialogueChoice>, TimeLimit: Float)` → `void`
```
Step 1: Filtered = GetValidChoices(Choices) ← filter by narrative flags
Step 2: If Filtered.Length == 0: auto-cancel (or select hidden default)
Step 3: Sort Filtered by Priority (descending)
Step 4: Set CurrentChoices = Filtered
Step 5: Set bChoiceActive = true
Step 6: Fire OnChoicesPresented(CurrentChoices) → WBP_DialogueChoiceDisplay shows UI
Step 7: If TimeLimit > 0:
Set ChoiceTimeLimit = TimeLimit, TimeRemaining = TimeLimit
Start looping timer (0.1s):
TimeRemaining -= 0.1 → update UI
If TimeRemaining <= 0 → Call OnChoiceTimedOut
```
#### `GetValidChoices(Choices)` → `Array<S_DialogueChoice>`
```
ForEach Choices:
If Choice.RequiredFlagTag valid:
Call BPC_NarrativeStateSystem.GetFlag(Choice.RequiredFlagTag)
If flag is true → Add to results
Else: Add to results (no requirement)
Return results
```
#### `SelectChoice(ChoiceIndex: Integer)` → `void`
```
Step 1: Validate ChoiceIndex < CurrentChoices.Length
Step 2: Selected = CurrentChoices[ChoiceIndex]
Step 3: Clear ChoiceTimer
Step 4: Fire OnChoiceSelected(Selected, ChoiceIndex)
Step 5: Call ProcessSelectedChoice(Selected)
```
#### `ProcessSelectedChoice(Choice)` → `GameplayTag` (NextSequenceTag)
```
Step 1: If Choice.ResultFlagTag valid:
BPC_NarrativeStateSystem.SetFlag(Choice.ResultFlagTag, true)
Step 2: Set bChoiceActive = false
Step 3: Fire OnChoiceCompleted(Choice.NextSequenceTag)
Step 4: Return Choice.NextSequenceTag ← caller (DialoguePlayback) plays this sequence
```
### Build Checklist
- [ ] Create BPC_DialogueChoiceSystem, add to Player Character
- [ ] Define S_DialogueChoice struct
- [ ] Implement PresentChoices with flag filtering + timer
- [ ] Implement GetValidChoices with RequiredFlagTag check
- [ ] Implement SelectChoice with timer clear
- [ ] Implement ProcessSelectedChoice: set flag → return next sequence
- [ ] Wire to WBP_DialogueChoiceDisplay via dispatcher
- [ ] Test: dialogue choice appears → player selects → flag set → next dialogue plays

View File

@@ -124,4 +124,55 @@ Evaluates narrative consequences when flags change. Listens for flag changes acr
| [`BPC_LoreUnlockSystem`](46_BPC_LoreUnlockSystem.md) | Direct | Unlock lore entries | | [`BPC_LoreUnlockSystem`](46_BPC_LoreUnlockSystem.md) | Direct | Unlock lore entries |
### Reuse Notes ### Reuse Notes
Consequence rules are data-driven via `DA_ConsequenceRule`, enabling designers to author branch logic without blueprint edits. The system supports both immediate and delayed consequences, allowing dramatic pacing. Priority sorting ensures critical consequences fire first. Blocking flag prevents race conditions between sequential narrative beats. Consequence rules are data-driven via `DA_ConsequenceRule`, enabling designers to author branch logic without blueprint edits. The system supports both immediate and delayed consequences, allowing dramatic pacing. Priority sorting ensures critical consequences fire first. Blocking flag prevents race conditions between sequential narrative beats.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_BranchingConsequenceSystem`
2. Add to Player Character
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set ConsequenceRules = empty array (loaded from DA_ConsequenceRule registry)
├─ Set PendingConsequences = empty array
├─ Set MaxConcurrentConsequences = 3
└─ Cache: BPC_NarrativeStateSystem, BPC_DialoguePlaybackSystem, BPC_ObjectiveSystem, BPC_CutsceneBridge, BPC_LoreUnlockSystem
```
### Function Node-by-Node
#### `OnFlagChanged(Fage: GameplayTag, NewValue: Boolean)` → `void` *(Bind to NarrativeStateSystem.OnFlagChanged)*
```
Step 1: If NewValue == false → Return (only react to true flags)
Step 2: ForEach ConsequenceRules:
If Rule.TriggerFlag == Flag:
Create FConsequencePayload(RuleRef=Rule, TriggerFlag=Flag, bIsImmediate=Rule.bIsImmediate, FireTime=Now+Delay)
If bIsImmediate: Add to pending, call ProcessPendingConsequences
Else: Add to PendingConsequences queue with timer
```
#### `ProcessPendingConsequences()` → `void`
```
Step 1: Sort PendingConsequences by Rule.Priority (descending)
Step 2: ForEach PendingConsequences (up to MaxConcurrentConsequences):
Switch on Rule.ActionType:
PlayDialogue → BPC_DialoguePlaybackSystem.PlaySequence(Rule.DialogueSequence)
SetObjective → BPC_ObjectiveSystem.ActivateObjective(Rule.ObjectiveTag)
UnlockLore → BPC_LoreUnlockSystem.UnlockEntry(Rule.LoreTag)
TriggerCutscene → BPC_CutsceneBridge.PlayCutscene(Rule.CutsceneData)
SetFlag → BPC_NarrativeStateSystem.SetFlag(Rule.FlagTag, true)
GrantItem → BPC_InventorySystem.AddItem(Rule.ItemAsset, 1)
Step 3: Remove processed from queue
```
### Build Checklist
- [ ] Create component, add to Player Character
- [ ] Bind to BPC_NarrativeStateSystem.OnFlagChanged
- [ ] Load DA_ConsequenceRule assets into ConsequenceRules array
- [ ] Implement OnFlagChanged: match trigger flag → create payload
- [ ] Implement ProcessPendingConsequences with action switch
- [ ] Create DA_ConsequenceRule Data Asset type with action enum and fields

View File

@@ -128,4 +128,61 @@ Manages trial-based gameplay scenarios: combat gauntlets, escape sequences, inve
| Phase 8 AI | Indirect | Spawn/despawn enemy actors by tag | | Phase 8 AI | Indirect | Spawn/despawn enemy actors by tag |
### Reuse Notes ### Reuse Notes
Scenarios are fully data-driven via `DA_ScenarioData`. Designers configure spawn tags, objective tags, time limits, and cleanup actions without blueprint modifications. Supports mixed scenarios: combat + investigation, timed + untimed, survival + objective. Scenarios are fully data-driven via `DA_ScenarioData`. Designers configure spawn tags, objective tags, time limits, and cleanup actions without blueprint modifications. Supports mixed scenarios: combat + investigation, timed + untimed, survival + objective.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_TrialScenarioSystem`
2. Add to Player Character or GameMode
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set ScenarioState = Inactive
└─ Cache: BPC_NarrativeStateSystem, BPC_ObjectiveSystem
```
### Function Node-by-Node
#### `StartScenario(ScenarioData: DA_ScenarioData)` → `void`
```
Step 1: If ScenarioState != Inactive → Return (already running)
Step 2: Set ActiveScenario = ScenarioData, ScenarioState = SetupRunning
Step 3: Create checkpoint via SS_SaveManager (auto-save before trial)
Step 4: Spawn enemies: ForEach ScenarioData.SpawnTags → Spawn Actor from Class at SpawnPoints
Step 5: Lock doors/block exits: ForEach ScenarioData.BlockActor → Set enabled=false
Step 6: If ScenarioData.bShowObjective: ActivateObjective(ScenarioData.ObjectiveTag)
Step 7: Set ScenarioStartTime = Get Game Time
Step 8: ScenarioState = ActiveRunning
Step 9: Fire OnScenarioStarted
```
#### `EvaluateScenario()` → `void` *(Called on Tick or timer)*
```
Step 1: If ScenarioState != ActiveRunning → Return
Step 2: Check success conditions:
If ScenarioData.SuccessFlags: HasAllFlags? → Call OnScenarioSuccess()
If ScenarioData.SuccessKillCount: enemies killed >= count? → Success
Step 3: Check failure conditions:
If ScenarioData.TimeLimit > 0 AND (Now - StartTime) >= TimeLimit → OnScenarioFailure("Time expired")
If player death detected → OnScenarioFailure("Player died")
```
#### `OnScenarioSuccess()` → `void`
```
Step 1: ScenarioState = Success
Step 2: ForEach ScenarioData.SuccessFlags → BPC_NarrativeStateSystem.SetFlag(flag, true)
Step 3: If Objective active: CompleteObjective(ObjectiveTag)
Step 4: Fire OnScenarioCompleted(true)
Step 5: Call CleanupScenario()
```
### Build Checklist
- [ ] Create component with DA_ScenarioData reference
- [ ] Implement StartScenario: setup → spawn → lock → objective → active
- [ ] Implement EvaluateScenario: check win/loss conditions per tick
- [ ] Implement OnScenarioSuccess/Failure with flag setting
- [ ] Implement CleanupScenario: despawn enemies, unlock doors

View File

@@ -135,4 +135,61 @@ Manages the discovery, unlocking, and tracking of lore entries (documents, notes
| [Save System](../04-save/32_SS_SaveManager.md) | Direct (via I_Persistable) | Save unlocked/read state | | [Save System](../04-save/32_SS_SaveManager.md) | Direct (via I_Persistable) | Save unlocked/read state |
### Reuse Notes ### Reuse Notes
Pure data-driven: all lore content is defined in `DA_LoreEntryData` assets. The system requires zero blueprint changes to add new lore entries. Supports any mix of discovery methods (pickup, narrative flag, location trigger, consequence). Lore categories are defined by gameplay tags, allowing flexible grouping without code changes. Pure data-driven: all lore content is defined in `DA_LoreEntryData` assets. The system requires zero blueprint changes to add new lore entries. Supports any mix of discovery methods (pickup, narrative flag, location trigger, consequence). Lore categories are defined by gameplay tags, allowing flexible grouping without code changes.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_LoreUnlockSystem`
2. Add to Player Character
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set AllLoreEntries = loaded DA_LoreEntryData array
├─ Set UnlockedLoreTags = empty Set<GameplayTag>
├─ Set ReadLoreTags = empty Set<GameplayTag>
├─ Set TotalLoreCount = AllLoreEntries.Length
└─ Bind to BPC_NarrativeStateSystem.OnFlagChanged → check for lore unlock triggers
```
### Function Node-by-Node
#### `UnlockLoreByTag(LoreTag: GameplayTag)` → `void`
```
Step 1: If UnlockedLoreTags.Contains(LoreTag) → Return (already unlocked)
Step 2: UnlockedLoreTags.Add(LoreTag)
Step 3: Find entry in AllLoreEntries by LoreTag
Step 4: Add to NewLoreQueue for notification
Step 5: Increment TotalUnlockedCount
Step 6: Fire OnLoreUnlocked(LoreTag, EntryData)
Step 7: Show notification: "New lore discovered: {Entry.Title}"
```
#### `MarkLoreAsRead(LoreTag: GameplayTag)` → `void`
```
Step 1: If NOT UnlockedLoreTags.Contains(LoreTag) → Return
Step 2: If NOT ReadLoreTags.Contains(LoreTag):
ReadLoreTags.Add(LoreTag)
Decrement UnreadCount
Fire OnLoreRead(LoreTag)
```
#### `GetLoreByCategory(CategoryTag: GameplayTag)` → `Array<DA_LoreEntryData>` *(Pure)*
```
ForEach AllLoreEntries:
If Entry.CategoryTag == CategoryTag AND UnlockedLoreTags.Contains(Entry.LoreTag):
Add to results
Return results
```
### Build Checklist
- [ ] Create BPC_LoreUnlockSystem, add to Player Character
- [ ] Define DA_LoreEntryData Data Asset (Title, Body, Category, RequiredFlag, LoreTag)
- [ ] Implement UnlockLoreByTag with dedup check
- [ ] Implement MarkLoreAsRead with unread counter
- [ ] Implement category-based filtering for UI
- [ ] Bind to NarrativeStateSystem for flag-triggered unlocks
- [ ] Test: pick up lore item → notification appears → journal entry unlocks

View File

@@ -156,4 +156,45 @@ A level-placed trigger volume that detects player overlap and fires narrative ac
### Reuse Notes ### Reuse Notes
This is the primary level-design tool for narrative gating. Designers place volumes, configure action lists, and set prerequisite flags — no blueprint editing required. Supports all narrative action types. The `CustomEvent` action type allows designers to bind custom level blueprint logic when no predefined action fits. This is the primary level-design tool for narrative gating. Designers place volumes, configure action lists, and set prerequisite flags — no blueprint editing required. Supports all narrative action types. The `CustomEvent` action type allows designers to bind custom level blueprint logic when no predefined action fits.
- Renamed from `47_BPC_NarrativeTriggerVolume` to `BP_NarrativeTriggerVolume` per Master naming convention. - Renamed from `47_BPC_NarrativeTriggerVolume` to `BP_NarrativeTriggerVolume` per Master naming convention.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `Actor`, Name = `BP_NarrativeTriggerVolume`
2. Add `BoxComponent` as Root (name: `CollisionBox`)
3. Set Collision Preset = `OverlapOnlyPawn`
### Function Node-by-Node
#### `Event ActorBeginOverlap(OtherActor)` → `void`
```
Step 1: If NOT bIsEnabled → Return
Step 2: Cast OtherActor to Player Character → if fail, Return
Step 3: Check RequiredFlags → if any missing, Return
Step 4: Check ExclusiveFlags → if any set, Return
Step 5: TriggerType == Once AND bHasTriggered? → Return
Step 6: Cooldown active? → Return
Step 7: Call ExecuteActions(TriggerActions)
Step 8: Set bHasTriggered = true, LastTriggerTime = Now
```
#### `ExecuteActions(Actions: Array<FTriggerActionEntry>)` → `void`
```
ForEach Actions → Switch on ActionType:
SetFlag → NarrativeState.SetFlag(Tag, true)
PlayDialogue → DialoguePlayback.PlaySequence(Asset)
PlayCutscene → CutsceneBridge.PlayCutscene(Asset)
ActivateObjective → ObjectiveSystem.ActivateObjective(Tag)
StartTrial → TrialScenario.StartScenario(Asset)
UnlockLore → LoreUnlock.UnlockLoreByTag(Tag)
GrantItem → Inventory.AddItem(Asset, 1)
```
### Build Checklist
- [ ] Create BP_NarrativeTriggerVolume with BoxComponent
- [ ] Define ETriggerActionType enum and FTriggerActionEntry struct
- [ ] Bind ActorBeginOverlap → check conditions → ExecuteActions
- [ ] Test: walk into volume → dialogue plays → flag sets → one-shot prevents re-fire

View File

@@ -133,4 +133,66 @@ Evaluates all narrative flags accumulated throughout gameplay and determines whi
Completely data-driven. Designers create DA_EndingData assets with required flags, exclusive flags, score thresholds, and score entries. The priority system ensures specific endings (e.g., "True Ending") override general ones. The system supports both linear and score-based ending evaluation simultaneously. Completely data-driven. Designers create DA_EndingData assets with required flags, exclusive flags, score thresholds, and score entries. The priority system ensures specific endings (e.g., "True Ending") override general ones. The system supports both linear and score-based ending evaluation simultaneously.
- Renamed from `45_BPC_EndingAccumulatorSystem` to `BPC_EndingAccumulator` per Master naming convention. - Renamed from `45_BPC_EndingAccumulatorSystem` to `BPC_EndingAccumulator` per Master naming convention.
- Cross-references updated: `BPC_CheckpointSystem``BP_Checkpoint`. - Cross-references updated: `BPC_CheckpointSystem``BP_Checkpoint`.
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_EndingAccumulator`
2. Add to Player Character or GameState
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set EndingScores = empty Map<GameplayTag, Float>
├─ Load all DA_EndingData assets into EndingDefinitions array
└─ Bind to BPC_NarrativeStateSystem.OnFlagChanged
```
### Function Node-by-Node
#### `OnNarrativeFlagChanged(Flag: GameplayTag, NewValue: Boolean)` → `void`
```
Step 1: If NewValue == false → Return (only accumulate on true flags)
Step 2: ForEach EndingDefinitions:
ForEach Entry in Ending.ScoreEntries:
If Entry.FlagTag == Flag:
EndingScores[Ending.EndingTag] += Entry.Weight
Break inner loop
Step 3: Fire OnEndingScoresUpdated(EndingScores)
```
#### `EvaluateEndings()` → `DA_EndingData`
```
Step 1: QualifiedList = empty array
Step 2: ForEach EndingDefinitions:
If EndingScores[Ending.EndingTag] >= Ending.RequiredScore:
Check Ending.RequiredFlags: all set? → Add to QualifiedList
Check Ending.ExclusiveFlags: any set? → Skip
Step 3: Sort QualifiedList by Priority (descending)
Step 4: Return QualifiedList[0] (highest priority qualified ending)
```
#### `GetQualifiedEndings()` → `Array<DA_EndingData>` *(Pure)*
```
Same logic as EvaluateEndings but returns all qualified, not just best
```
#### `TriggerGameEnding(OverrideTag: GameplayTag)` → `void`
```
Step 1: If OverrideTag valid → SelectedEnding = Find(OverrideTag)
Else: SelectedEnding = EvaluateEndings()
Step 2: Fire OnGameEndingTriggered(SelectedEnding)
Step 3: GM_CoreGameMode.TriggerEnding(SelectedEnding.EndingTag)
→ initiates cutscene, game-over screen, credits
```
### Build Checklist
- [ ] Create BPC_EndingAccumulator
- [ ] Define DA_EndingData with ScoreEntries, RequiredFlags, ExclusiveFlags, RequiredScore, Priority
- [ ] Bind to NarrativeStateSystem.OnFlagChanged → accumulate scores
- [ ] Implement EvaluateEndings with qualification gating
- [ ] Wire to GM_CoreGameMode.TriggerEnding
- [ ] Test: make choices throughout game → check ending scores → trigger end → correct ending plays

View File

@@ -189,4 +189,65 @@ flowchart TD
--- ---
*Specification based on Master Section 5.7, line 1821.* *Specification based on Master Section 5.7, line 1821.*
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_HitReactionSystem`
2. Add to Player Character (or EnemyBase)
3. Create HitReactionMontages per damage type: Physical, Fire, Explosive, etc.
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set CurrentTrauma = 0.0, bIsReacting = false
├─ Populate HitReactionMontages map (E_DamageType → AnimMontage)
├─ Cache: BPC_HealthSystem (bind OnDamageTaken), BPC_CameraStateLayer
└─ Start TraumaDecay timer (0.1s loop)
```
### Function Node-by-Node
#### `ProcessHitReaction(DamageEvent: S_DamageEvent)` → `void`
```
Step 1: If bIsReacting → Return (already playing reaction)
Step 2: Set LastDamageDirection = DamageEvent.HitNormal * -1 ← direction damage came from
Step 3: If DamageEvent.Amount >= RagdollThreshold:
Play ragdoll blend: ABP → Set bRagdoll=true, physics blend
Set bIsReacting = true → timer to recover
Return
Step 4: If DamageEvent.Amount >= FlinchThreshold:
Select montage: HitReactionMontages[DamageEvent.DamageType]
If multiple: pick NextReactionIndex, cycle it
Play Montage with blend from LastDamageDirection
Set bIsReacting = true
Step 5: Call ApplyTrauma(DamageEvent.Amount * 0.5)
Step 6: Fire OnHitReactionPlayed(DamageEvent)
Step 7: On montage complete → Set bIsReacting = false
```
#### `ApplyTrauma(Amount: Float)` → `void`
```
Step 1: CurrentTrauma = Min(100, CurrentTrauma + Amount)
Step 2: Push to camera: BPC_CameraStateLayer.ApplyPostProcessOverride(TraumaVignette, CurrentTrauma/100)
Step 3: Fire OnTraumaChanged(CurrentTrauma)
```
#### `TraumaDecay Tick` → `void` *(Timer 0.1s)*
```
CurrentTrauma = Max(0, CurrentTrauma - TraumaDecayRate * 0.1)
Update camera effect intensity
If zero: clear post-process override
```
### Build Checklist
- [ ] Create BPC_HitReactionSystem, add to Player Character
- [ ] Create hit reaction montages per damage type
- [ ] Implement ProcessHitReaction with threshold branching
- [ ] Implement ApplyTrauma with camera effect push
- [ ] Set up trauma decay timer
- [ ] Bind to BPC_HealthSystem.OnDamageTaken
- [ ] Test: take small hit → flinch; take big hit → ragdoll; trauma decays over time

View File

@@ -49,4 +49,53 @@
- RecoilPattern is a CurveVector from DA_WeaponData — swap per weapon - RecoilPattern is a CurveVector from DA_WeaponData — swap per weapon
- For hitscan weapons, call ApplyRecoil on every shot. For projectile, call on fire release - For hitscan weapons, call ApplyRecoil on every shot. For projectile, call on fire release
- Recovery runs on tick when bIsRecovering; disable tick at rest for performance - Recovery runs on tick when bIsRecovering; disable tick at rest for performance
---
## Manual Implementation Guide
### Class Setup
1. Create Blueprint Class: Parent = `ActorComponent`, Name = `BPC_RecoilSystem`
2. Attach to `BP_RangedWeapon` or Player Character
3. **⚠️ Local only** — recoil is cosmetic, not replicated
### Variable Init (BeginPlay)
```
Event BeginPlay
├─ Set CurrentRecoilOffset = (0, 0)
├─ Set bIsRecovering = false
├─ Read RecoilPattern from DA_WeaponData
└─ Cache: BPC_CameraStateLayer (from Player)
```
### Function Node-by-Node
#### `ApplyRecoil(ShotCount: Integer, bADS: Boolean)` → `void`
```
Step 1: Sample RecoilPattern curve at ShotCount (or accumulated counter)
Step 2: Get curve output: Pitch = X channel, Yaw = Y channel
Step 3: If bADS: Pitch *= ADSRecoilMultiplier, Yaw *= ADSRecoilMultiplier
Step 4: CurrentRecoilOffset.X += Pitch, CurrentRecoilOffset.Y += Yaw
Step 5: Push to camera:
BPC_CameraStateLayer.AddControllerPitch(Pitch) ← or PlayCameraShake
BPC_CameraStateLayer.AddControllerYaw(Yaw)
Step 6: Set bIsRecovering = true
Step 7: Set Timer (0.05s loop) → TickRecovery ← timer runs while recovering
```
#### `TickRecovery(DeltaTime: Float)` → `void`
```
Step 1: CurrentRecoilOffset = Lerp(CurrentRecoilOffset, Vector2D(0,0), RecoverySpeed * DeltaTime)
Step 2: If CurrentRecoilOffset nearly zero (< 0.01):
ResetRecoil()
Clear Recovery timer
```
### Build Checklist
- [ ] Create BPC_RecoilSystem, attach to Player
- [ ] Create RecoilPattern CurveVector per weapon (X=pitch, Y=yaw)
- [ ] Implement ApplyRecoil with curve sampling + ADS multiplier
- [ ] Implement TickRecovery with lerp toward zero
- [ ] Wire to BPC_FirearmSystem.OnFire
- [ ] Test: fire weapon → camera kicks up → recovers to center