Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Xml.Linq;
8 
9 namespace Barotrauma
10 {
11 
16  {
17 
18  public enum DialogTypes
19  {
20  Regular,
21  Small,
22  Mission
23  }
24 
25  const float InterruptDistance = 300.0f;
26 
31  const float BlockOtherConversationsDuration = 5.0f;
32 
33  [Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")]
34  public string Text { get; set; }
35 
36  [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")]
37  public Identifier SpeakerTag { get; set; }
38 
39  [Serialize("", IsPropertySaveable.Yes, description: "Tag of the player the conversation is shown to. If empty, the conversation is shown to everyone. If SpeakerTag is defined, the conversation is always only shown to the player who interacts with the speaker.")]
40  public Identifier TargetTag { get; set; }
41 
42  [Serialize(true, IsPropertySaveable.Yes, "Should someone interact with the speaker for the conversation to trigger?")]
43  public bool WaitForInteraction { get; set; }
44 
45  [Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation.")]
46  public Identifier InvokerTag { get; set; }
47 
48  [Serialize(false, IsPropertySaveable.Yes, description: "Should the screen fade to black when the conversation is active?")]
49  public bool FadeToBlack { get; set; }
50 
51  [Serialize(true, IsPropertySaveable.Yes, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")]
52  public bool EndEventIfInterrupted { get; set; }
53 
54  [Serialize("", IsPropertySaveable.Yes, description: "Identifier of an event sprite to display in the corner of the conversation prompt.")]
55  public string EventSprite { get; set; }
56 
57  [Serialize(DialogTypes.Regular, IsPropertySaveable.Yes, description: "Type of the dialog prompt.")]
58  public DialogTypes DialogType { get; set; }
59 
60  [Serialize(false, IsPropertySaveable.Yes, description: "Does this conversation continue after this ConversationAction? If you have multiple successive ConversationActions, perhaps with some actions happening in between, you can enable this to prevent the dialog prompt from closing between the actions. Not necessary if the ConversationActions are nested inside each other: those are always considered parts of the same conversation, and shown in the same prompt.")]
61  public bool ContinueConversation { get; set; }
62 
63  [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")]
64  public bool ContinueAutomatically { get; set; }
65 
66  [Serialize(false, IsPropertySaveable.Yes, description: "If SpeakerTag is defined, the conversation is interrupted by default if the speaker and the target end up too far from each other. This can be used to disable that behavior, keeping the dialog prompt open regardless of the distance.")]
67  public bool IgnoreInterruptDistance { get; set; }
68 
70  {
71  get;
72  private set;
73  }
74 
75  private AIObjective prevIdleObjective, prevGotoObjective;
76  private AIObjective npcWaitObjective;
77 
78  public List<SubactionGroup> Options { get; private set; }
79 
80  public SubactionGroup Interrupted { get; private set; }
81 
82  private static UInt16 actionCount;
83 
84  //an identifier the server uses to identify which ConversationAction a client is responding to
85  public readonly UInt16 Identifier;
86 
87  private int selectedOption = -1;
88  private bool dialogOpened = false;
89 
90  private double lastActiveTime;
91 
92  private bool interrupt;
93 
94  private readonly XElement textElement;
95 
96  public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
97  {
98  actionCount++;
99  Identifier = actionCount;
100  Options = new List<SubactionGroup>();
101  foreach (var elem in element.Elements())
102  {
103  if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase))
104  {
105  Options.Add(new SubactionGroup(ParentEvent, elem));
106  }
107  else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase))
108  {
110  }
111  else if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase))
112  {
113  Text = elem.GetAttributeString("tag", string.Empty);
114  textElement = elem;
115  }
116  }
117  if (element.GetChildElement("Replace") != null)
118  {
119  DebugConsole.ThrowError(
120  $"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
121  $" - unrecognized child element \"Replace\".",
122  contentPackage: element.ContentPackage);
123  }
124  }
125 
127  {
128  LocalizedString text = string.Empty;
129 
130  if (textElement != null)
131  {
132  TextManager.ConstructDescription(ref text, textElement, ParentEvent.GetTextForReplacementElement);
133  }
134  else
135  {
136  text = TextManager.Get(Text).Fallback(Text);
137  if (text.Value.IsNullOrEmpty())
138  {
139  text = text.Fallback(Text);
140  }
141  }
143  }
144 
145  public override IEnumerable<EventAction> GetSubActions()
146  {
147  return Options.SelectMany(group => group.Actions);
148  }
149 
150  public override bool IsFinished(ref string goTo)
151  {
152  if (interrupt)
153  {
154  if (dialogOpened)
155  {
156 #if CLIENT
157  dialogBox?.Close();
158  GUIMessageBox.MessageBoxes.ForEachMod(mb =>
159  {
160  if (mb.UserData as string == "ConversationAction")
161  {
162  (mb as GUIMessageBox)?.Close();
163  }
164  });
165 #else
166  foreach (Client c in GameMain.Server.ConnectedClients)
167  {
168  if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); }
169  }
170 #endif
171  ResetSpeaker();
172  dialogOpened = false;
173  }
174 
175  if (Interrupted == null)
176  {
177  if (EndEventIfInterrupted) { goTo = "_end"; }
178  return true;
179  }
180  else
181  {
182  return Interrupted.IsFinished(ref goTo);
183  }
184  }
185 
186  if (ContinueAutomatically && Options.None())
187  {
188  return dialogOpened;
189  }
190 
191  if (selectedOption >= 0)
192  {
193  if (Options.None() || Options[selectedOption].IsFinished(ref goTo))
194  {
195  ResetSpeaker();
196  return true;
197  }
198  }
199  return false;
200  }
201 
202  public override void Reset()
203  {
204  Options.ForEach(a => a.Reset());
205  ResetSpeaker();
206  selectedOption = -1;
207  interrupt = false;
208  dialogOpened = false;
209  Speaker = null;
210  }
211 
212  public override bool SetGoToTarget(string goTo)
213  {
214  selectedOption = -1;
215  for (int i = 0; i < Options.Count; i++)
216  {
217  if (Options[i].SetGoToTarget(goTo))
218  {
219  selectedOption = i;
220  interrupt = false;
221  dialogOpened = true;
222  return true;
223  }
224  }
225  return false;
226  }
227 
228  private void ResetSpeaker()
229  {
230  if (Speaker == null) { return; }
231  Speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
233  Speaker.SetCustomInteract(null, null);
234 #if SERVER
235  GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData());
236 #endif
237  if (Speaker.AIController is HumanAIController humanAI && !Speaker.IsDead && !Speaker.Removed)
238  {
239  humanAI.ClearForcedOrder();
240  if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
241  if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
242  humanAI.ObjectiveManager.SortObjectives();
243  }
244  }
245 
246  public int[] GetEndingOptions()
247  {
248  List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
249  if (!ContinueConversation) { endings.Add(-1); }
250  return endings.ToArray();
251  }
252 
253  public override void Update(float deltaTime)
254  {
255  if (interrupt)
256  {
257  Interrupted?.Update(deltaTime);
258  }
259  else if (selectedOption < 0)
260  {
261  if (dialogOpened)
262  {
263  lastActiveTime = Timing.TotalTime;
264 #if CLIENT
265  if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction"))
266  {
267  Character.DisableControls = true;
268  }
269  else
270  {
271  Reset();
272  }
273 #endif
274  if (ShouldInterrupt(requireTarget: true))
275  {
276  ResetSpeaker();
277  interrupt = true;
278  }
279  return;
280  }
281 
282  if (!SpeakerTag.IsEmpty)
283  {
284  if (npcWaitObjective != null)
285  {
286  npcWaitObjective.ForceHighestPriority = true;
287  }
288  if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
289  Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
290  if (Speaker == null || Speaker.Removed)
291  {
292  return;
293  }
294  //some conversation already assigned to the speaker, wait for it to be removed
296  {
297  return;
298  }
299  else if (!WaitForInteraction)
300  {
301  TryStartConversation(Speaker);
302  }
303  else if (Speaker.ActiveConversation != this)
304  {
307 #if CLIENT
309  TryStartConversation,
310  TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
311 #else
313  TryStartConversation,
314  TextManager.Get("CampaignInteraction.Talk"));
316 #endif
317  }
318  return;
319  }
320  else
321  {
322  TryStartConversation(null);
323  }
324  }
325  else
326  {
327  //after the conversation has been finished and the target character assigned,
328  //we no longer care if we still have a target
329  if (ShouldInterrupt(requireTarget: false))
330  {
331  ResetSpeaker();
332  interrupt = true;
333  }
334  else if (Options.Any())
335  {
336  Options[selectedOption].Update(deltaTime);
337  }
338  }
339  }
340 
341  private bool ShouldInterrupt(bool requireTarget)
342  {
343  IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
344  if (!TargetTag.IsEmpty && requireTarget)
345  {
346  targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e, requireTarget));
347  if (!targets.Any()) { return true; }
348  }
349 
350  if (Speaker != null)
351  {
352  if (!TargetTag.IsEmpty && requireTarget && !IgnoreInterruptDistance)
353  {
354  if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, Speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
355  }
356  if (Speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
357  {
358  return true;
359  }
361  }
362 
363  return false;
364  }
365 
366  private bool IsValidTarget(Entity e, bool requirePlayerControlled = true)
367  {
368  bool isValid =
369  e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
370  (character == Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled);
371 #if SERVER
372  if (!dialogOpened)
373  {
374  UpdateIgnoredClients();
375  isValid &= !ignoredClients.Keys.Any(c => c.Character == e);
376  }
377 #elif CLIENT
378  bool block = GUI.InputBlockingMenuOpen && !dialogOpened;
379  isValid &= (e != Character.Controlled || !block);
380 #endif
381  return isValid;
382  }
383 
384  private void TryStartConversation(Character speaker, Character targetCharacter = null)
385  {
386  IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
387  if (!TargetTag.IsEmpty)
388  {
389  targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
390  if (!targets.Any() || IsBlockedByAnotherConversation(targets, BlockOtherConversationsDuration)) { return; }
391  }
392 
393  if (targetCharacter != null && IsBlockedByAnotherConversation(targetCharacter.ToEnumerable(), 0.1f)) { return; }
394 
395  if (speaker?.AIController is HumanAIController humanAI)
396  {
397  prevIdleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
398  prevGotoObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveGoTo>();
399  npcWaitObjective = humanAI.SetForcedOrder(
400  new Order(OrderPrefab.Prefabs["wait"], Barotrauma.Identifier.Empty, null, orderGiver: null));
401  if (targets.Any() || targetCharacter != null)
402  {
403  Entity closestTarget = targetCharacter;
404  float closestDist = float.MaxValue;
405  foreach (Entity entity in targets)
406  {
407  float dist = Vector2.DistanceSquared(entity.WorldPosition, speaker.WorldPosition);
408  if (dist < closestDist)
409  {
410  closestTarget = entity;
411  closestDist = dist;
412  }
413  }
414  if (closestTarget != null)
415  {
416  humanAI.FaceTarget(closestTarget);
417  }
418  }
419  }
420 
421  if (targetCharacter != null && !InvokerTag.IsEmpty)
422  {
423  ParentEvent.AddTarget(InvokerTag, targetCharacter);
424  }
425 
426  ShowDialog(speaker, targetCharacter);
427 
428  dialogOpened = true;
429  if (speaker != null)
430  {
431  speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
432  speaker.SetCustomInteract(null, null);
433 #if SERVER
434  GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
435 #endif
436  }
437  }
438 
439  partial void ShowDialog(Character speaker, Character targetCharacter);
440 
441  public override string ToDebugString()
442  {
443  if (!interrupt)
444  {
445  return $"{ToolBox.GetDebugSymbol(selectedOption > -1, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Selected option: {selectedOption.ColorizeObject()})";
446  }
447  else
448  {
449  return $"{ToolBox.GetDebugSymbol(true, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Interrupted)";
450  }
451  }
452  }
453 }
void SetCustomInteract(Action< Character, Character > onCustomInteract, LocalizedString hudText)
Set an action that's invoked when another character interacts with this one.
Triggers a "conversation popup" with text and support for different branching options.
override bool IsFinished(ref string goTo)
Has the action finished.
ConversationAction(ScriptedEvent parentEvent, ContentXElement element)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
bool IsFinished(ref string goTo)
Definition: EventAction.cs:48
readonly ScriptedEvent ParentEvent
Definition: EventAction.cs:102
static readonly List< GUIComponent > MessageBoxes
static NetworkMember NetworkMember
Definition: GameMain.cs:190
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
virtual LocalizedString ReplaceVariablesInEventText(LocalizedString str)
void AddTarget(Identifier tag, Entity target)
IEnumerable< Entity > GetTargets(Identifier tag)
virtual string GetTextForReplacementElement(string tag)