2 using System.Collections.Generic;
6 using System.Collections.Immutable;
12 public static readonly Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>
Collections =
new Dictionary<LanguageIdentifier, PrefabCollection<NPCConversationCollection>>();
22 foreach (var subElement
in element.Elements())
25 if (elemName ==
"Conversation")
37 const int MaxPreviousConversations = 20;
39 public readonly
string Line;
43 public readonly ImmutableHashSet<Identifier>
Flags;
49 public readonly ImmutableArray<NPCConversation>
Responses;
50 private readonly
int speakerIndex;
51 private readonly ImmutableHashSet<Identifier> allowedSpeakerTags;
52 private readonly
bool requireNextLine;
56 Line = element.GetAttributeString(
"line",
"");
58 speakerIndex = element.GetAttributeInt(
"speaker", 0);
60 AllowedJobs = element.GetAttributeIdentifierArray(
"allowedjobs", Array.Empty<Identifier>()).ToImmutableHashSet();
61 Flags = element.GetAttributeIdentifierArray(
"flags", Array.Empty<Identifier>()).ToImmutableHashSet();
62 allowedSpeakerTags = element.GetAttributeIdentifierArray(
"speakertags", Array.Empty<Identifier>()).ToImmutableHashSet();
64 if (element.Attribute(
"minintensity") !=
null) minIntensity = element.GetAttributeFloat(
"minintensity", 0.0f);
65 if (element.Attribute(
"maxintensity") !=
null)
maxIntensity = element.GetAttributeFloat(
"maxintensity", 1.0f);
68 requireNextLine = element.GetAttributeBool(
"requirenextline",
false);
71 private static List<Identifier> GetCurrentFlags(
Character speaker)
73 var currentFlags =
new List<Identifier>();
76 if (GameMain.GameSession !=
null && Level.Loaded !=
null)
78 if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
80 if (GameMain.GameSession.RoundDuration < 30.0f) { currentFlags.Add(
"Initial".ToIdentifier()); }
82 else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
84 if (GameMain.GameSession.RoundDuration < 120.0f &&
85 speaker?.CurrentHull !=
null &&
86 GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
90 currentFlags.Add(
"EnterOutpost".ToIdentifier());
92 if (Level.Loaded.IsEndBiome)
94 currentFlags.Add(
"EndLevel".ToIdentifier());
97 if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f)
99 currentFlags.Add(
"Casual".ToIdentifier());
102 if (GameMain.GameSession.IsCurrentLocationRadiated())
104 currentFlags.Add(
"InRadiation".ToIdentifier());
111 currentFlags.Add((speaker.
CurrentHull ==
null ?
"Outside" :
"Inside").ToIdentifier());
115 if (
Character.Controlled.CharacterHealth.GetAffliction(
"psychosis") !=
null)
117 currentFlags.Add((speaker !=
Character.Controlled ?
"Psychosis" :
"PsychosisSelf").ToIdentifier());
122 foreach (Affliction affliction
in afflictions)
124 var currentEffect = affliction.GetActiveEffect();
125 if (currentEffect is { DialogFlag.IsEmpty:
false } && !currentFlags.Contains(currentEffect.DialogFlag))
127 currentFlags.Add(currentEffect.DialogFlag);
133 currentFlags.Add(
"OutpostNPC".ToIdentifier());
134 if (GameMain.GameSession?.Level?.StartLocation?.Faction is Faction faction)
136 currentFlags.Add($
"OutpostNPC{faction.Prefab.Identifier}".ToIdentifier());
141 currentFlags.Add($
"CampaignNPC.{speaker.CampaignInteractionType}".ToIdentifier());
143 if (GameMain.GameSession?.GameMode is CampaignMode campaignMode &&
144 (campaignMode.Map?.CurrentLocation?.Type?.Identifier ==
"abandoned"))
148 currentFlags.Add(
"Bandit".ToIdentifier());
152 currentFlags.Add(
"Hostage".ToIdentifier());
157 currentFlags.Add(
"escort".ToIdentifier());
164 private static readonly List<NPCConversation> previousConversations =
new List<NPCConversation>();
168 Dictionary<int, Character> assignedSpeakers =
new Dictionary<int, Character>();
169 List<(
Character speaker,
string line)> lines =
new List<(
Character speaker,
string line)>();
171 var language = GameSettings.CurrentConfig.Language;
174 DebugConsole.AddWarning($
"Could not find NPC conversations for the language \"{language}\". Using \"{TextManager.DefaultLanguage}\" instead..");
175 language = TextManager.DefaultLanguage;
178 CreateConversation(availableSpeakers, assignedSpeakers,
null, lines,
183 public static List<(
Character speaker,
string line)>
CreateRandom(List<Character> availableSpeakers, IEnumerable<Identifier> requiredFlags)
185 Dictionary<int, Character> assignedSpeakers =
new Dictionary<int, Character>();
186 List<(
Character speaker,
string line)> lines =
new List<(
Character speaker,
string line)>();
188 var language = GameSettings.CurrentConfig.Language;
191 DebugConsole.AddWarning($
"Could not find NPC conversations for the language \"{language}\". Using \"{TextManager.DefaultLanguage}\" instead..");
192 language = TextManager.DefaultLanguage;
196 .SelectMany(cc => cc.Conversations.Where(c => requiredFlags.All(f => c.Flags.Contains(f)))).ToList();
197 if (availableConversations.Count > 0)
199 CreateConversation(availableSpeakers, assignedSpeakers,
null, lines, availableConversations: availableConversations, ignoreFlags:
false);
204 private static void CreateConversation(
205 List<Character> availableSpeakers,
206 Dictionary<int, Character> assignedSpeakers,
208 IList<(
Character speaker,
string line)> lineList,
209 IList<NPCConversation> availableConversations,
210 bool ignoreFlags =
false)
212 IList<NPCConversation> conversations = baseConversation ==
null ? availableConversations : baseConversation.
Responses;
213 if (conversations.Count == 0) {
return; }
215 int conversationIndex = Rand.Int(conversations.Count);
216 NPCConversation selectedConversation = conversations[conversationIndex];
217 if (
string.IsNullOrEmpty(selectedConversation.Line)) {
return; }
221 if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
224 var characterFlags = GetCurrentFlags(assignedSpeakers[selectedConversation.speakerIndex]);
225 if (selectedConversation.Flags.All(flag => characterFlags.Contains(flag)))
227 speaker = assignedSpeakers[selectedConversation.speakerIndex];
232 var allowedSpeakers =
new List<Character>();
234 List<NPCConversation> potentialLines =
new List<NPCConversation>(conversations);
237 if (GameMain.GameSession?.EventManager !=
null)
239 potentialLines.RemoveAll(l =>
240 (l.minIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity < l.minIntensity) ||
241 (l.maxIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity > l.maxIntensity));
244 while (potentialLines.Count > 0)
248 selectedConversation = GetRandomConversation(potentialLines, baseConversation ==
null);
249 if (selectedConversation ==
null ||
string.IsNullOrEmpty(selectedConversation.Line)) {
return; }
252 if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
254 speaker = assignedSpeakers[selectedConversation.speakerIndex];
258 foreach (Character potentialSpeaker
in availableSpeakers)
260 if (CheckSpeakerViability(potentialSpeaker, selectedConversation, assignedSpeakers.Values.ToList(), ignoreFlags))
262 allowedSpeakers.Add(potentialSpeaker);
266 if (allowedSpeakers.Count == 0 || NextLineFailure(selectedConversation, availableSpeakers, allowedSpeakers, ignoreFlags))
268 allowedSpeakers.Clear();
269 potentialLines.Remove(selectedConversation);
277 if (allowedSpeakers.Count == 0) {
return; }
278 speaker = allowedSpeakers[Rand.Int(allowedSpeakers.Count)];
279 availableSpeakers.Remove(speaker);
280 assignedSpeakers.Add(selectedConversation.speakerIndex, speaker);
283 if (baseConversation ==
null)
285 previousConversations.Insert(0, selectedConversation);
286 if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
288 lineList.Add((speaker, selectedConversation.Line));
289 CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
292 static bool NextLineFailure(
NPCConversation selectedConversation, List<Character> availableSpeakers, List<Character> allowedSpeakers,
bool ignoreFlags)
294 if (selectedConversation.requireNextLine)
296 foreach (
NPCConversation nextConversation
in selectedConversation.Responses)
298 foreach (Character potentialNextSpeaker
in availableSpeakers)
300 if (CheckSpeakerViability(potentialNextSpeaker, nextConversation, allowedSpeakers, ignoreFlags))
311 static bool CheckSpeakerViability(Character potentialSpeaker,
NPCConversation selectedConversation, List<Character> checkedSpeakers,
bool ignoreFlags)
314 if ((potentialSpeaker.Info?.Job !=
null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0)
316 if (!(potentialSpeaker.Info?.Job?.Prefab is { } speakerJobPrefab)
317 || !selectedConversation.AllowedJobs.Contains(speakerJobPrefab.Identifier)) {
return false; }
323 var characterFlags = GetCurrentFlags(potentialSpeaker);
324 if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) {
return false; }
328 if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) {
return false; }
331 if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeTarget(s))) {
return false; }
334 if (selectedConversation.allowedSpeakerTags.Count > 0)
336 if (potentialSpeaker.Info?.PersonalityTrait ==
null) {
return false; }
337 if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) {
return false; }
341 if (potentialSpeaker.Info?.PersonalityTrait !=
null &&
342 !potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains(
"none"))
349 private static NPCConversation GetRandomConversation(List<NPCConversation> conversations,
bool avoidPreviouslyUsed)
351 if (!avoidPreviouslyUsed)
353 return conversations.Count == 0 ? null : conversations[Rand.Int(conversations.Count)];
356 List<float> probabilities =
new List<float>();
359 probabilities.Add(GetConversationProbability(conversation));
361 return ToolBox.SelectWeightedRandom(conversations, probabilities, Rand.RandSync.Unsynced);
364 private static float GetConversationProbability(
NPCConversation conversation)
367 float baseProbability = MathF.Pow(conversation.Flags.Count + 1, 2);
369 int index = previousConversations.IndexOf(conversation);
370 if (index < 0) {
return baseProbability * 10.0f; }
372 return baseProbability + 1.0f - 1.0f / (index + 1);
376 public static void WriteToCSV()
378 System.Text.StringBuilder sb =
new System.Text.StringBuilder();
380 foreach (Identifier identifier
in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language].Keys)
382 foreach (var current
in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language][identifier].Conversations)
384 WriteConversation(sb, current, 0);
385 WriteSubConversations(sb, current.Responses, 1);
390 File.WriteAllText(
"NPCConversations.csv", sb.ToString());
393 private static void WriteConversation(System.Text.StringBuilder sb,
NPCConversation conv,
int depthIndex)
395 sb.Append(conv.speakerIndex);
397 sb.Append(depthIndex);
399 sb.Append(conv.Line);
403 sb.Append(
string.Join(
",", conv.Flags));
406 sb.Append(
string.Join(
',', conv.AllowedJobs));
409 sb.Append(
string.Join(
",", conv.allowedSpeakerTags));
411 sb.Append(conv.minIntensity);
413 sb.Append(conv.maxIntensity);
419 private static void WriteSubConversations(System.Text.StringBuilder sb, IList<NPCConversation> responses,
int depthIndex)
421 for (
int i = 0; i < responses.Count; i++)
423 WriteConversation(sb, responses[i], depthIndex);
427 WriteSubConversations(sb, responses[i].
Responses, depthIndex + 1);
432 private static void WriteEmptyRow(System.Text.StringBuilder sb)
434 for (
int i = 0; i < 8; i++)
IReadOnlyCollection< Affliction > GetAllAfflictions()
CharacterHealth CharacterHealth
CampaignMode.InteractionType CampaignInteractionType
readonly AnimController AnimController
Identifier GetAttributeIdentifier(string key, string def)
NPCConversationCollection(NPCConversationsFile file, ContentXElement element)
readonly List< NPCConversation > Conversations
readonly LanguageIdentifier Language
static readonly Dictionary< LanguageIdentifier, PrefabCollection< NPCConversationCollection > > Collections
static List<(Character speaker, string line)> CreateRandom(List< Character > availableSpeakers)
NPCConversation(XElement element)
readonly? float maxIntensity
static List<(Character speaker, string line)> CreateRandom(List< Character > availableSpeakers, IEnumerable< Identifier > requiredFlags)
readonly ImmutableArray< NPCConversation > Responses
readonly ImmutableHashSet< Identifier > AllowedJobs
readonly ImmutableHashSet< Identifier > Flags
readonly Identifier Identifier
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
@ Character
Characters only