Client LuaCsForBarotrauma
TriggerAction.cs
1 using Microsoft.Xna.Framework;
2 using System.Collections.Generic;
3 using System.Linq;
4 
5 namespace Barotrauma
6 {
11  {
12  public enum TriggerType
13  {
14  Inside,
15  Outside
16  }
17 
18  [Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")]
19  public Identifier Target1Tag { get; set; }
20 
21  [Serialize("", IsPropertySaveable.Yes, description: "Tag of the second entity that will be used for trigger checks.")]
22  public Identifier Target2Tag { get; set; }
23 
24  [Serialize("", IsPropertySaveable.Yes, description: "If set, the first target has to be within an outpost module of this type.")]
25  public Identifier TargetModuleType { get; set; }
26 
27  [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first entity when the trigger check succeeds.")]
28  public Identifier ApplyToTarget1 { get; set; }
29 
30  [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")]
31  public Identifier ApplyToTarget2 { get; set; }
32 
33  [Serialize(TriggerType.Inside, IsPropertySaveable.Yes, description: "Determines if the targets must be inside or outside of the radius.")]
34  public TriggerType Type { get; set; }
35 
36  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range to activate the trigger.")]
37  public float Radius { get; set; }
38 
39  [Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")]
40  public bool DisableInCombat { get; set; }
41 
42  [Serialize(true, IsPropertySaveable.Yes, description: "If true, dead/unconscious characters cannot trigger the action.")]
43  public bool DisableIfTargetIncapacitated { get; set; }
44 
45  [Serialize(false, IsPropertySaveable.Yes, description: "If true, one target must interact with the other to trigger the action.")]
46  public bool WaitForInteraction { get; set; }
47 
48  [Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")]
49  public bool AllowMultipleTargets { get; set; }
50 
51  [Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")]
52  public bool CheckAllTargets { get; set; }
53 
54  [Serialize(false, IsPropertySaveable.Yes, description: "If true, interacting with the target will make the character select it.")]
55  public bool SelectOnTrigger { get; set; }
56 
57  private float distance;
58 
59  public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
60  {
61  if (element.GetAttribute(nameof(TagAction.IgnoreIncapacitatedCharacters)) != null)
62  {
63  DebugConsole.AddWarning(
64  $"Potential error in {nameof(TriggerAction)}, event \"{parentEvent.Prefab.Identifier}\": "+
65  $"{nameof(TagAction.IgnoreIncapacitatedCharacters)} is a property of {nameof(TagAction)}, did you mean {nameof(DisableIfTargetIncapacitated)}?",
66  contentPackage: element.ContentPackage);
67  }
68  }
69 
70  private bool isFinished = false;
71  public override bool IsFinished(ref string goTo)
72  {
73  return isFinished;
74  }
75  public override void Reset()
76  {
77  ResetTargetIcons();
78  isRunning = false;
79  isFinished = false;
80  }
81 
82  public bool isRunning = false;
83 
84  private readonly List<Either<Character, Item>> npcsOrItems = new List<Either<Character, Item>>();
85 
86  private readonly List<(Entity e1, Entity e2)> triggerers = new List<(Entity e1, Entity e2)>();
87 
88  public override void Update(float deltaTime)
89  {
90  if (isFinished) { return; }
91 
92  isRunning = true;
93 
94  var targets1 = ParentEvent.GetTargets(Target1Tag);
95  if (!targets1.Any()) { return; }
96 
97  triggerers.Clear();
98  foreach (Entity e1 in targets1)
99  {
100  if (DisableInCombat && IsInCombat(e1))
101  {
102  if (CheckAllTargets)
103  {
104  return;
105  }
106  continue;
107  }
108  if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated))
109  {
110  if (CheckAllTargets)
111  {
112  return;
113  }
114  continue;
115  }
116  if (!TargetModuleType.IsEmpty)
117  {
118  if (!CheckAllTargets && CheckDistanceToHull(e1, out Hull hull))
119  {
120  Trigger(e1, hull);
121  return;
122  }
123  else if (CheckAllTargets)
124  {
125  if (CheckDistanceToHull(e1, out hull))
126  {
127  triggerers.Add((e1, hull));
128  }
129  else
130  {
131  return;
132  }
133  }
134  continue;
135  }
136 
137  var targets2 = ParentEvent.GetTargets(Target2Tag);
138 
139  foreach (Entity e2 in targets2)
140  {
141  if (e1 == e2)
142  {
143  continue;
144  }
145  if (DisableInCombat && IsInCombat(e2))
146  {
147  if (CheckAllTargets)
148  {
149  return;
150  }
151  continue;
152  }
153  if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated))
154  {
155  if (CheckAllTargets)
156  {
157  return;
158  }
159  continue;
160  }
161 
162  if (WaitForInteraction)
163  {
164  Character player = null;
165  Character npc = null;
166  Item item = null;
167  if (e1 is Character char1)
168  {
169  if (char1.IsPlayer)
170  {
171  player = char1;
172  }
173  else
174  {
175  npc ??= char1;
176  }
177  }
178  else
179  {
180  item ??= e1 as Item;
181  }
182  if (e2 is Character char2)
183  {
184  if (char2.IsPlayer)
185  {
186  player = char2;
187  }
188  else
189  {
190  npc ??= char2;
191  }
192  }
193  else
194  {
195  item ??= e2 as Item;
196  }
197 
198  if (player != null)
199  {
200  if (npc != null)
201  {
202  if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc))
203  {
204  npcsOrItems.Add(npc);
205  }
207  {
208  //if the NPC has a conversation available, don't assign the trigger until the conversation is done
209  continue;
210  }
211  else if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
212  {
215 #if CLIENT
216  npc.SetCustomInteract(
217  (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
218  TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
219 #else
220  npc.SetCustomInteract(
221  (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
222  TextManager.Get("CampaignInteraction.Talk"));
224 #endif
225  }
226  if (!AllowMultipleTargets) { return; }
227  }
228  else if (item != null)
229  {
230  if (!npcsOrItems.Any(n => n.TryGet(out Item item2) && item2 == item))
231  {
232  npcsOrItems.Add(item);
233  }
235  GameMain.NetworkMember?.ConnectedClients.Where(c => c.Character != null && targets2.Contains(c.Character)));
236  if (player.SelectedItem == item ||
237  player.SelectedSecondaryItem == item ||
238  (player.Inventory != null && player.Inventory.Contains(item)) ||
239  (player.FocusedItem == item && player.IsKeyHit(InputType.Use)))
240  {
241  Trigger(e1, e2);
242  return;
243  }
244  }
245  }
246  }
247  else
248  {
249  Vector2 pos1 = e1.WorldPosition;
250  Vector2 pos2 = e2.WorldPosition;
251  distance = Vector2.Distance(pos1, pos2);
252  if ((Type == TriggerType.Inside) == IsWithinRadius())
253  {
254  if (!CheckAllTargets)
255  {
256  Trigger(e1, e2);
257  return;
258  }
259  else
260  {
261  triggerers.Add((e1, e2));
262  }
263  }
264  else if (CheckAllTargets)
265  {
266  return;
267  }
268 
269  bool IsWithinRadius() =>
270  ((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
271  ((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
272  Vector2.DistanceSquared(pos1, pos2) < Radius * Radius;
273  }
274  }
275  }
276 
277  foreach (var (e1, e2) in triggerers)
278  {
279  Trigger(e1, e2);
280  }
281  }
282 
283  private void ResetTargetIcons()
284  {
285  foreach (var npcOrItem in npcsOrItems)
286  {
287  if (npcOrItem.TryGet(out Character npc))
288  {
289  npc.CampaignInteractionType = CampaignMode.InteractionType.None;
290  npc.SetCustomInteract(null, null);
291  npc.RequireConsciousnessForCustomInteract = true;
292  #if SERVER
294  #endif
295  }
296  else if (npcOrItem.TryGet(out Item item))
297  {
298  item.AssignCampaignInteractionType(CampaignMode.InteractionType.None);
299  }
300  }
301  }
302 
303  private bool CheckDistanceToHull(Entity e, out Hull hull)
304  {
305  hull = null;
306  if (Radius <= 0)
307  {
308  if (e is Character character && character.CurrentHull != null && character.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
309  {
310  hull = character.CurrentHull;
311  return Type == TriggerType.Inside;
312  }
313  else if (e is Item item && item.CurrentHull != null && item.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
314  {
315  hull = item.CurrentHull;
316  return Type == TriggerType.Inside;
317  }
318  return Type == TriggerType.Outside;
319  }
320  else
321  {
322  foreach (Hull potentialHull in Hull.HullList)
323  {
324  if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; }
325  Rectangle hullRect = potentialHull.WorldRect;
326  hullRect.Inflate(Radius, Radius);
327  if (Submarine.RectContains(hullRect, e.WorldPosition))
328  {
329  hull = potentialHull;
330  return Type == TriggerType.Inside;
331  }
332  }
333  return Type == TriggerType.Outside;
334  }
335  }
336 
337  private static bool IsInCombat(Entity entity)
338  {
339  if (entity is not Character character) { return false; }
340  foreach (Character c in Character.CharacterList)
341  {
342  if (c.IsDead || c.Removed || c.IsIncapacitated || !c.Enabled) { continue; }
343  if (c.IsBot && c.AIController is HumanAIController humanAi)
344  {
345  if (humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective &&
346  combatObjective.Enemy == character)
347  {
348  return true;
349  }
350  }
351  else if (c.AIController is EnemyAIController { State: AIState.Aggressive or AIState.Attack } enemyAI)
352  {
353  if (enemyAI.SelectedAiTarget?.Entity == character || c.CurrentHull == character.CurrentHull)
354  {
355  return true;
356  }
357  }
358  }
359  return false;
360  }
361 
362  private void Trigger(Entity entity1, Entity entity2)
363  {
364  ResetTargetIcons();
365  if (!ApplyToTarget1.IsEmpty)
366  {
368  }
369  if (!ApplyToTarget2.IsEmpty)
370  {
372  }
373 
374  Character player = null;
375  Entity target = null;
376  if (entity1 is Character { IsPlayer: true })
377  {
378  player = entity1 as Character;
379  target = entity2;
380  }
381  else if (entity2 is Character { IsPlayer: true })
382  {
383  player = entity2 as Character;
384  target = entity1;
385  }
386  if (player != null && SelectOnTrigger)
387  {
388  if (target is Character targetCharacter)
389  {
390  player.SelectCharacter(targetCharacter);
391  }
392  else if (target is Item targetItem)
393  {
394  if (targetItem.IsSecondaryItem)
395  {
396  player.SelectedSecondaryItem = targetItem;
397  }
398  else
399  {
400  player.SelectedItem = targetItem;
401  }
402  }
403  }
404 
405  isRunning = false;
406  isFinished = true;
407  }
408 
409  public override string ToDebugString()
410  {
411  if (TargetModuleType.IsEmpty)
412  {
413  string targetStr = "none";
414  if (npcsOrItems.Any())
415  {
416  targetStr = string.Join(", ",
417  npcsOrItems.Select(npcOrItem =>
418  npcOrItem.TryGet(out Character character) ? character.Name : (npcOrItem.TryGet(out Item item) ? item.Name : "none")));
419  }
420 
421  return
422  $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" +
424  $"Selected non-player target: {targetStr.ColorizeObject()}, " :
425  $"Distance: {((int)distance).ColorizeObject()}, ") +
426  $"Radius: {Radius.ColorizeObject()}, " +
427  $"TargetTags: {Target1Tag.ColorizeObject()}, " +
428  $"{Target2Tag.ColorizeObject()})";
429  }
430  else
431  {
432  return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (TargetTags: {Target1Tag.ColorizeObject()}, {TargetModuleType.ColorizeObject()})";
433  }
434  }
435  }
436 }
void SetCustomInteract(Action< Character, Character > onCustomInteract, LocalizedString hudText)
Set an action that's invoked when another character interacts with this one.
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
Item SelectedSecondaryItem
The secondary selected item. It's an item other than a device (see SelectedItem), e....
ContentPackage? ContentPackage
XAttribute? GetAttribute(string name)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
readonly ScriptedEvent ParentEvent
Definition: EventAction.cs:106
static NetworkMember NetworkMember
Definition: GameMain.cs:190
bool Contains(Item item)
Is the item contained in this inventory. Does not recursively check items inside items.
void AssignCampaignInteractionType(CampaignMode.InteractionType interactionType, IEnumerable< Client > targetClients=null)
void AddTarget(Identifier tag, Entity target)
IEnumerable< Entity > GetTargets(Identifier tag)
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
Tags a specific entity. Tags are used by other actions to refer to specific entities....
Definition: TagAction.cs:13
bool IgnoreIncapacitatedCharacters
Definition: TagAction.cs:33
Waits for a player to trigger the action before continuing. Triggering can mean entering a specific t...
override bool IsFinished(ref string goTo)
Has the action finished.
override void Update(float deltaTime)
override string ToDebugString()
Rich test to display in debugdraw
override void Reset()
TriggerAction(ScriptedEvent parentEvent, ContentXElement element)
@ Character
Characters only