3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
25 const float InterruptDistance = 300.0f;
31 const float BlockOtherConversationsDuration = 5.0f;
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; }
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.")]
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.")]
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.")]
54 [
Serialize(
"",
IsPropertySaveable.Yes, description:
"Identifier of an event sprite to display in the corner of the conversation prompt.")]
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.")]
63 [
Serialize(
false,
IsPropertySaveable.Yes, description:
"If enabled, the event will not stop to wait for the conversation to be dismissed.")]
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.")]
75 private AIObjective prevIdleObjective, prevGotoObjective;
78 public List<SubactionGroup>
Options {
get;
private set; }
82 private static UInt16 actionCount;
87 private int selectedOption = -1;
88 private bool dialogOpened =
false;
90 private double lastActiveTime;
92 private bool interrupt;
94 private readonly XElement textElement;
100 Options =
new List<SubactionGroup>();
101 foreach (var elem
in element.Elements())
103 if (elem.Name.LocalName.Equals(
"option", StringComparison.OrdinalIgnoreCase))
107 else if (elem.Name.LocalName.Equals(
"interrupt", StringComparison.OrdinalIgnoreCase))
111 else if (elem.Name.LocalName.Equals(
"text", StringComparison.OrdinalIgnoreCase))
113 Text = elem.GetAttributeString(
"tag",
string.Empty);
117 if (element.GetChildElement(
"Replace") !=
null)
119 DebugConsole.ThrowError(
120 $
"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
121 $
" - unrecognized child element \"Replace\".",
122 contentPackage: element.ContentPackage);
130 if (textElement !=
null)
137 if (text.
Value.IsNullOrEmpty())
147 return Options.SelectMany(group => group.Actions);
160 if (mb.UserData as
string ==
"ConversationAction")
162 (mb as GUIMessageBox)?.Close();
172 dialogOpened =
false;
191 if (selectedOption >= 0)
193 if (
Options.None() ||
Options[selectedOption].IsFinished(ref goTo))
204 Options.ForEach(a => a.Reset());
208 dialogOpened =
false;
215 for (
int i = 0; i <
Options.Count; i++)
228 private void ResetSpeaker()
230 if (
Speaker ==
null) {
return; }
235 GameMain.NetworkMember.CreateEntityEvent(
Speaker,
new Character.AssignCampaignInteractionEventData());
239 humanAI.ClearForcedOrder();
240 if (prevIdleObjective !=
null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
241 if (prevGotoObjective !=
null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
242 humanAI.ObjectiveManager.SortObjectives();
248 List<int> endings =
Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group =>
Options.IndexOf(group)).ToList();
250 return endings.ToArray();
253 public override void Update(
float deltaTime)
259 else if (selectedOption < 0)
263 lastActiveTime = Timing.TotalTime;
274 if (ShouldInterrupt(requireTarget:
true))
284 if (npcWaitObjective !=
null)
309 TryStartConversation,
310 TextManager.GetWithVariable(
"CampaignInteraction.Talk",
"[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(
InputType.Use)));
313 TryStartConversation,
314 TextManager.Get(
"CampaignInteraction.Talk"));
322 TryStartConversation(
null);
329 if (ShouldInterrupt(requireTarget:
false))
336 Options[selectedOption].Update(deltaTime);
341 private bool ShouldInterrupt(
bool requireTarget)
343 IEnumerable<Entity> targets = Enumerable.Empty<
Entity>();
347 if (!targets.Any()) {
return true; }
354 if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition,
Speaker.
WorldPosition) > InterruptDistance * InterruptDistance)) {
return true; }
356 if (
Speaker.
AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
366 private bool IsValidTarget(Entity e,
bool requirePlayerControlled =
true)
369 e is
Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
370 (character ==
Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled);
374 UpdateIgnoredClients();
375 isValid &= !ignoredClients.Keys.Any(c => c.Character == e);
378 bool block = GUI.InputBlockingMenuOpen && !dialogOpened;
379 isValid &= (e !=
Character.Controlled || !block);
384 private void TryStartConversation(Character speaker, Character targetCharacter =
null)
386 IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
390 if (!targets.Any() || IsBlockedByAnotherConversation(targets, BlockOtherConversationsDuration)) {
return; }
393 if (targetCharacter !=
null && IsBlockedByAnotherConversation(targetCharacter.ToEnumerable(), 0.1f)) {
return; }
395 if (speaker?.AIController is HumanAIController humanAI)
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)
403 Entity closestTarget = targetCharacter;
404 float closestDist =
float.MaxValue;
405 foreach (Entity entity
in targets)
407 float dist = Vector2.DistanceSquared(entity.WorldPosition, speaker.WorldPosition);
408 if (dist < closestDist)
410 closestTarget = entity;
414 if (closestTarget !=
null)
416 humanAI.FaceTarget(closestTarget);
421 if (targetCharacter !=
null && !
InvokerTag.IsEmpty)
426 ShowDialog(speaker, targetCharacter);
431 speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
432 speaker.SetCustomInteract(
null,
null);
434 GameMain.NetworkMember.CreateEntityEvent(speaker,
new Character.AssignCampaignInteractionEventData());
439 partial
void ShowDialog(Character speaker, Character targetCharacter);
445 return $
"{ToolBox.GetDebugSymbol(selectedOption > -1, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Selected option: {selectedOption.ColorizeObject()})";
449 return $
"{ToolBox.GetDebugSymbol(true, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Interrupted)";
bool ForceHighestPriority
void SetCustomInteract(Action< Character, Character > onCustomInteract, LocalizedString hudText)
Set an action that's invoked when another character interacts with this one.
virtual AIController AIController
CampaignMode.InteractionType CampaignInteractionType
static bool DisableControls
ConversationAction ActiveConversation
Triggers a "conversation popup" with text and support for different branching options.
override bool IsFinished(ref string goTo)
Has the action finished.
override bool SetGoToTarget(string goTo)
readonly UInt16 Identifier
LocalizedString GetDisplayText()
List< SubactionGroup > Options
bool EndEventIfInterrupted
override IEnumerable< EventAction > GetSubActions()
bool ContinueAutomatically
SubactionGroup Interrupted
ConversationAction(ScriptedEvent parentEvent, ContentXElement element)
bool ContinueConversation
bool IgnoreInterruptDistance
override string ToDebugString()
Rich test to display in debugdraw
override void Update(float deltaTime)
virtual Vector2 WorldPosition
void Update(float deltaTime)
bool IsFinished(ref string goTo)
readonly ScriptedEvent ParentEvent
static readonly List< GUIComponent > MessageBoxes
static NetworkMember NetworkMember
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)