Unstable 1.1.14.0

This commit is contained in:
Markus Isberg
2023-10-02 16:43:54 +03:00
parent 94f5a93a0c
commit cf8f0de659
606 changed files with 21906 additions and 11456 deletions
@@ -12,6 +12,9 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; } = Identifier.Empty;
[Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction.")]
public Identifier SourceCharacter { get; set; } = Identifier.Empty;
[Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")]
public LimbType TargetLimb { get; set; }
@@ -33,8 +36,7 @@ namespace Barotrauma
if (target.CharacterHealth == null) { continue; }
if (TargetLimb == LimbType.None)
{
var affliction = target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions);
if (affliction != null && affliction.Strength >= MinStrength) { return true; }
if (target.CharacterHealth.GetAfflictionStrengthByIdentifier(Identifier, AllowLimbAfflictions) >= MinStrength) { return true; }
}
IEnumerable<Affliction> afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction =>
{
@@ -43,9 +45,13 @@ namespace Barotrauma
LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type;
if (limbType == null || limbType != TargetLimb) { return false; }
}
if (!SourceCharacter.IsEmpty)
{
if (!ParentEvent.GetTargets(SourceCharacter).Contains(affliction.Source)) { return false; }
}
return affliction.Strength >= MinStrength;
});
if (afflictions.Any(a => a.Identifier == Identifier)) { return true; }
}
return false;
@@ -46,7 +46,7 @@ namespace Barotrauma
}
if (target == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.");
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.");
}
if (target == null || Conditional == null)
{
@@ -85,7 +85,7 @@ namespace Barotrauma
case bool bool1 when metadata2 is bool bool2:
return CompareBool(bool1, bool2) ?? false;
case float float1 when metadata2 is float float2:
return CompareFloat(float1, float2) ?? false;
return PropertyConditional.CompareFloat(float1, float2, Operator);
}
}
@@ -143,36 +143,13 @@ namespace Barotrauma
{
if (float.TryParse(value, out float f))
{
return CompareFloat(GetFloat(campaignMode), f);
return PropertyConditional.CompareFloat(GetFloat(campaignMode), f, Operator);
}
DebugConsole.Log($"{value} != float");
return null;
}
private bool? CompareFloat(float val1, float val2)
{
value1 = val1;
value2 = val2;
switch (Operator)
{
case PropertyConditional.ComparisonOperatorType.Equals:
return MathUtils.NearlyEqual(val1, val2);
case PropertyConditional.ComparisonOperatorType.GreaterThan:
return val1 > val2;
case PropertyConditional.ComparisonOperatorType.GreaterThanEquals:
return val1 >= val2;
case PropertyConditional.ComparisonOperatorType.LessThan:
return val1 < val2;
case PropertyConditional.ComparisonOperatorType.LessThanEquals:
return val1 <= val2;
case PropertyConditional.ComparisonOperatorType.NotEquals:
return !MathUtils.NearlyEqual(val1, val2);
}
return null;
}
private bool? TryString(CampaignMode campaignMode, string value)
{
return CompareString(GetString(campaignMode), value);
@@ -1,8 +1,8 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -20,9 +20,15 @@ namespace Barotrauma
[Serialize(1, IsPropertySaveable.Yes)]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")]
public Identifier HullTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first target when the check succeeds.")]
public Identifier ApplyTagToTarget { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the found item(s) when the check succeeds.")]
public Identifier ApplyTagToItem { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool RequireEquipped { get; set; }
@@ -32,6 +38,24 @@ namespace Barotrauma
[Serialize(-1, IsPropertySaveable.Yes)]
public int ItemContainerIndex { get; set; }
private readonly bool checkPercentage;
private float requiredConditionalMatchPercentage;
[Serialize(100.0f, IsPropertySaveable.Yes)]
/// <summary>
/// What percentage of targets do the conditionals need to match for the check to succeed?
/// </summary>
public float RequiredConditionalMatchPercentage
{
get { return requiredConditionalMatchPercentage; }
set { requiredConditionalMatchPercentage = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
[Serialize(false, IsPropertySaveable.Yes)]
public bool CompareToInitialAmount { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
private readonly Identifier[] itemIdentifierSplit;
@@ -49,16 +73,84 @@ namespace Barotrauma
}
conditionals = conditionalList;
if (itemTags.None() && ItemIdentifiers.None())
if (itemTags.None() &&
ItemIdentifiers.None() &&
TargetTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check.");
}
checkPercentage = element.GetAttribute(nameof(RequiredConditionalMatchPercentage)) is not null;
if (Amount != 1 && checkPercentage)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}.");
}
}
private bool EnoughTargets(int totalTargets, int targetsWithConditionalsMatched)
{
if (CompareToInitialAmount)
{
totalTargets = ParentEvent.GetInitialTargetCount(TargetTag);
}
if (checkPercentage)
{
return MathUtils.Percentage(targetsWithConditionalsMatched, totalTargets) >= RequiredConditionalMatchPercentage;
}
else
{
return targetsWithConditionalsMatched >= Amount;
}
}
private readonly List<Item> tempTargetItems = new List<Item>();
protected override bool? DetermineSuccess()
{
var targets = ParentEvent.GetTargets(TargetTag);
if (!targets.Any()) { return null; }
if (!HullTag.IsEmpty)
{
var hulls = ParentEvent.GetTargets(HullTag).OfType<Hull>();
targets = targets.Where(t =>
(t is Item it && hulls.Contains(it.CurrentHull)) ||
(t is Character c && hulls.Contains(c.CurrentHull)));
}
if (!targets.Any())
{
if (conditionals.Any())
{
//conditionals can't be met if there's no targets
return false;
}
return null;
}
//check if the target(s) are the items we're looking for (instead of characters/containers the items are inside)
int targetCount = targets.Count();
if (targetCount >= Amount)
{
tempTargetItems.Clear();
foreach (var target in targets)
{
if (target is not Item item) { continue; }
if (itemTags.Any() && itemTags.None(item.HasTag) &&
itemIdentifierSplit.Any() && !itemIdentifierSplit.Contains(item.Prefab.Identifier))
{
continue;
}
if (ConditionalsMatch(item, character: null))
{
tempTargetItems.Add(item);
}
}
if (EnoughTargets(targetCount, tempTargetItems.Count))
{
TryApplyTagToItems(tempTargetItems);
return true;
}
}
foreach (var target in targets)
{
if (target is Character character)
@@ -99,8 +191,9 @@ namespace Barotrauma
private bool CheckInventory(Inventory inventory, Character character)
{
if (inventory == null) { return false; }
int count = 0;
int targetCount = 0;
HashSet<Item> eventTargets = new HashSet<Item>();
tempTargetItems.Clear();
foreach (Identifier tag in itemTags)
{
foreach (var target in ParentEvent.GetTargets(tag))
@@ -117,19 +210,39 @@ namespace Barotrauma
eventTargets.Contains(it),
recursive: Recursive))
{
if (!ConditionalsMatch(item, character)) { continue; }
count++;
if (count >= Amount) { return true; }
targetCount++;
if (ConditionalsMatch(item, character))
{
tempTargetItems.Add(item);
}
}
if (EnoughTargets(targetCount, tempTargetItems.Count))
{
TryApplyTagToItems(tempTargetItems);
return true;
}
return false;
}
private void TryApplyTagToItems(IEnumerable<Item> items)
{
if (!ApplyTagToItem.IsEmpty)
{
foreach (var targetItem in items)
{
ParentEvent.AddTarget(ApplyTagToItem, targetItem);
}
}
}
private bool ConditionalsMatch(Item item, Character character = null)
{
if (item == null) { return false; }
foreach (PropertyConditional conditional in conditionals)
{
if (!conditional.Matches(item))
{
if (!item.ConditionalMatches(conditional))
{
return false;
}
@@ -145,8 +258,8 @@ namespace Barotrauma
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" +
$"Succeeded: {succeeded.ColorizeObject()})";
(ItemTags.Any() ? $"ItemTags: {ItemTags.ColorizeObject()}, " : $"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}, ") +
$"Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -0,0 +1,35 @@
#nullable enable
namespace Barotrauma
{
class CheckTraitorEventStateAction : BinaryOptionAction
{
[Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)]
public TraitorEvent.State State { get; set; }
private readonly TraitorEvent? traitorEvent;
public CheckTraitorEventStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (parentEvent is TraitorEvent traitorEvent)
{
this.traitorEvent = traitorEvent;
}
else
{
DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.");
}
}
protected override bool? DetermineSuccess()
{
return traitorEvent?.CurrentState == State;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckTraitorEventStateAction)} -> " +
$"State: {State.ColorizeObject()}, Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -0,0 +1,40 @@
#nullable enable
using Barotrauma.Networking;
using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Checks whether the specific target was voted as the traitor.
/// </summary>
class CheckTraitorVoteAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Target { get; set; }
public CheckTraitorVoteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (parentEvent is not TraitorEvent)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events.");
}
}
protected override bool? DetermineSuccess()
{
var targetEntities = ParentEvent.GetTargets(Target);
#if SERVER
if (GameMain.Server?.TraitorManager?.GetClientAccusedAsTraitor() is Client traitorClient)
{
return targetEntities.Any(e => e is Character character && traitorClient?.Character == character);
}
#endif
return false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckTraitorVoteAction)} -> (TargetTag: {Target.ColorizeObject()}";
}
}
}
@@ -0,0 +1,60 @@
#nullable enable
namespace Barotrauma
{
class CheckVisibilityAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")]
public Identifier EntityTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")]
public Identifier TargetTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")]
public bool CheckFacing { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")]
public Identifier ApplyTagToEntity { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity that was seen when the check succeeds.")]
public Identifier ApplyTagToTarget { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "If both the seeing entity and the target are the same, does it count as success?")]
public bool AllowSameEntity { get; set; }
public CheckVisibilityAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
protected override bool? DetermineSuccess()
{
foreach (var entity in ParentEvent.GetTargets(EntityTag))
{
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (!AllowSameEntity && entity == target) { continue; }
if (Character.IsTargetVisible(target, entity, CheckFacing))
{
if (!ApplyTagToEntity.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToEntity, entity);
}
if (!ApplyTagToTarget.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToTarget, target);
}
return true;
}
}
}
return false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckVisibilityAction)} -> (TargetTags: {EntityTag.ColorizeObject()}, {TargetTag.ColorizeObject()})";
}
}
}
@@ -43,13 +43,14 @@ namespace Barotrauma
foreach (var npc in affectedNpcs)
{
if (!(npc.AIController is HumanAIController humanAiController)) { continue; }
if (npc.Removed) { continue; }
if (npc.AIController is not HumanAIController humanAiController) { continue; }
Character enemy = null;
float closestDist = float.MaxValue;
foreach (Entity target in ParentEvent.GetTargets(EnemyTag))
{
if (!(target is Character character)) { continue; }
if (target is not Character character) { continue; }
float dist = Vector2.DistanceSquared(npc.WorldPosition, target.WorldPosition);
if (dist < closestDist)
{
@@ -82,7 +83,7 @@ namespace Barotrauma
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; }
foreach (var combatObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveCombat>())
{
combatObjective.Abandon = true;
@@ -59,6 +59,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool ContinueConversation { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")]
public bool ContinueAutomatically { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool IgnoreInterruptDistance { get; set; }
@@ -86,6 +89,8 @@ namespace Barotrauma
private bool interrupt;
private readonly XElement textElement;
public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
actionCount++;
@@ -93,15 +98,41 @@ namespace Barotrauma
Options = new List<SubactionGroup>();
foreach (var elem in element.Elements())
{
if (elem.Name.LocalName.Equals("option", StringComparison.InvariantCultureIgnoreCase))
if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase))
{
Options.Add(new SubactionGroup(ParentEvent, elem));
}
else if (elem.Name.LocalName.Equals("interrupt", StringComparison.InvariantCultureIgnoreCase))
else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase))
{
Interrupted = new SubactionGroup(ParentEvent, elem);
}
else if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase))
{
Text = elem.GetAttributeString("tag", string.Empty);
textElement = elem;
}
}
if (element.GetChildElement("Replace") != null)
{
DebugConsole.ThrowError(
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
$" - unrecognized child element \"Replace\".");
}
}
public LocalizedString GetDisplayText()
{
LocalizedString text = string.Empty;
if (textElement != null)
{
TextManager.ConstructDescription(ref text, textElement, ParentEvent.GetTextForReplacementElement);
}
else
{
text = TextManager.Get(Text).Fallback(Text);
}
return ParentEvent.ReplaceVariablesInEventText(text);
}
public override IEnumerable<EventAction> GetSubActions()
@@ -145,9 +176,14 @@ namespace Barotrauma
}
}
if (ContinueAutomatically && Options.None())
{
return dialogOpened;
}
if (selectedOption >= 0)
{
if (!Options.Any() || Options[selectedOption].IsFinished(ref goTo))
if (Options.None() || Options[selectedOption].IsFinished(ref goTo))
{
ResetSpeaker();
return true;
@@ -0,0 +1,120 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class CountTargetsAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Optional second tag. Can be used if the target must have two different tags.")]
public Identifier SecondRequiredTargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")]
public Identifier HullTag { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
public int MinAmount { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
public int MaxAmount { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CompareToTarget { get; set; }
[Serialize(-1.0f, IsPropertySaveable.Yes)]
/// <summary>
/// Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget
/// E.g. you could compare the number of entities tagged as "discoveredhull" to entities tagged as "anyhull" to require 50% of hulls to be discovered.
/// </summary>
public float MinPercentageRelativeToTarget { get; set; }
[Serialize(-1.0f, IsPropertySaveable.Yes)]
/// <summary>
/// Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget
/// E.g. you could compare the number of entities tagged as "floodedhull" to entities tagged as "anyhull" to require less than 50% of hulls to be flooded.
/// </summary>
public float MaxPercentageRelativeToTarget { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
public CountTargetsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
var conditionalList = new List<PropertyConditional>();
foreach (ContentXElement subElement in element.GetChildElements("conditional"))
{
conditionalList.AddRange(PropertyConditional.FromXElement(subElement!));
}
conditionals = conditionalList;
if (CompareToTarget.IsEmpty)
{
int amount = element.GetAttributeInt("amount", -1);
if (amount > -1)
{
MinAmount = MaxAmount = amount;
}
if (MinAmount > MaxAmount && MaxAmount > -1)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}.");
}
}
else
{
if (MinPercentageRelativeToTarget < 0.0f && MaxPercentageRelativeToTarget < 0.0f)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set.");
}
}
}
protected override bool? DetermineSuccess()
{
var potentialTargets = ParentEvent.GetTargets(TargetTag);
if (!SecondRequiredTargetTag.IsEmpty)
{
potentialTargets = potentialTargets.Where(t => ParentEvent.GetTargets(SecondRequiredTargetTag).Contains(t));
}
if (!HullTag.IsEmpty)
{
var hulls = ParentEvent.GetTargets(HullTag).OfType<Hull>();
potentialTargets = potentialTargets.Where(t =>
(t is Item it && hulls.Contains(it.CurrentHull)) ||
(t is Character c && hulls.Contains(c.CurrentHull)));
}
if (conditionals.Any())
{
potentialTargets = potentialTargets.Where(t => conditionals.Any(c => c.Matches(t as ISerializableEntity)));
}
int targetCount = potentialTargets.Count();
if (CompareToTarget.IsEmpty)
{
if (MinAmount > -1 && targetCount < MinAmount) { return false; }
if (MaxAmount > -1 && targetCount > MaxAmount) { return false; }
}
else
{
int compareToTargetCount = ParentEvent.GetTargets(CompareToTarget).Count();
float percentage = MathUtils.Percentage(targetCount, compareToTargetCount);
if (MinPercentageRelativeToTarget > -1 && percentage < MinPercentageRelativeToTarget) { return false; }
if (MaxPercentageRelativeToTarget > -1 && percentage > MaxPercentageRelativeToTarget) { return false; }
}
return true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CountTargetsAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -138,12 +137,17 @@ namespace Barotrauma
Type actionType;
try
{
actionType = Type.GetType("Barotrauma." + element.Name, true, true);
Identifier typeName = element.Name.ToString().ToIdentifier();
if (typeName == "TutorialSegmentAction")
{
typeName = "EventObjectiveAction".ToIdentifier();
}
actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true);
if (actionType == null) { throw new NullReferenceException(); }
}
catch
{
DebugConsole.ThrowError("Could not find an event class of the type \"" + element.Name + "\".");
DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\".");
return null;
}
@@ -0,0 +1,94 @@
#nullable enable
using System;
using System.Xml.Linq;
namespace Barotrauma
{
partial class EventLogAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Id { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public string Text { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
public bool ShowInServerLog { get; set; }
private readonly XElement? textElement;
public EventLogAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (Id == Identifier.Empty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no id.");
}
//append the target tag so logs targeted to different players don't interfere with each other even if they use the same Id
Id = (Id.ToString() + TargetTag).ToIdentifier();
foreach (var elem in element.Elements())
{
if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase))
{
textElement = elem;
break;
}
}
Text ??= string.Empty;
if (textElement == null)
{
if (Text.IsNullOrEmpty())
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\". {nameof(EventLogAction)} with no text set ({element}).");
}
else
{
Text = TextManager.Get(Text).Fallback(Text).Value;
}
}
ShowInServerLog = element.GetAttributeBool(nameof(ShowInServerLog), ParentEvent is TraitorEvent);
}
private bool isFinished;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public LocalizedString GetDisplayText()
{
LocalizedString text = Text;
if (textElement != null)
{
LocalizedString tempDescription = string.Empty;
TextManager.ConstructDescription(ref tempDescription, textElement, ParentEvent.GetTextForReplacementElement);
text = tempDescription.Value;
}
return ParentEvent.ReplaceVariablesInEventText(text);
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
AddEntryProjSpecific(GameMain.GameSession?.EventManager?.EventLog, GetDisplayText().Value);
isFinished = true;
}
partial void AddEntryProjSpecific(EventLog? eventLog, string displayText);
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(EventLogAction)} -> (Id: {Id})";
}
}
}
@@ -1,8 +1,8 @@
namespace Barotrauma
{
partial class TutorialSegmentAction : EventAction
partial class EventObjectiveAction : EventAction
{
public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove };
public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove };
[Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)]
public SegmentActionType Type { get; set; }
@@ -34,14 +34,29 @@ namespace Barotrauma
[Serialize(80, IsPropertySaveable.Yes)]
public int Height { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
private bool isFinished;
public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
public EventObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (Identifier.IsEmpty)
{
Identifier = element.GetAttributeIdentifier("id", Identifier.Empty);
}
if (Type != SegmentActionType.Trigger && !TextTag.IsEmpty)
{
DebugConsole.ThrowError(
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\""+
$" - {nameof(TextTag)} will do nothing unless the action triggers a message box or a video.");
}
if (element.GetChildElement("Replace") != null)
{
DebugConsole.ThrowError(
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
$" - unrecognized child element \"Replace\".");
}
}
public override void Update(float deltaTime)
@@ -0,0 +1,49 @@
using System.Linq;
namespace Barotrauma
{
class GiveExpAction : EventAction
{
[Serialize(0, IsPropertySaveable.Yes)]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
public GiveExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (TargetTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveExpAction)} without a target tag (the action needs to know whose skill to check).");
}
}
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character);
foreach (var target in targets)
{
target.Info?.GiveExperience(Amount);
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(GiveExpAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"Amount: {Amount.ColorizeObject()})";
}
}
}
@@ -1,6 +1,4 @@
using Microsoft.Xna.Framework;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -19,7 +17,7 @@ namespace Barotrauma
{
if (TargetTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": GiveSkillExpAction without a target tag (the action needs to know whose skill to check).");
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(GiveSkillExpAction)} without a target tag (the action needs to know whose skill to check).");
}
}
@@ -1,5 +1,3 @@
using System.Xml.Linq;
namespace Barotrauma
{
class GoTo : EventAction
@@ -100,7 +100,7 @@ namespace Barotrauma
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human);
if (subWaypoint != null)
{
npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true);
npc.GiveIdCardTags(subWaypoint, requireSpawnPointTagsNotGiven: false, createNetworkEvent: true);
}
}
}
@@ -25,8 +25,8 @@ namespace Barotrauma
public NPCFollowAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
private List<Character> affectedNpcs = null;
private Entity target = null;
private IEnumerable<Character> affectedNpcs;
private Entity target;
public override void Update(float deltaTime)
{
@@ -36,9 +36,10 @@ namespace Barotrauma
if (target == null) { return; }
int targetCount = 0;
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character);
foreach (var npc in affectedNpcs)
{
if (npc.Removed) { continue; }
if (npc.AIController is not HumanAIController humanAiController) { continue; }
if (Follow)
@@ -48,6 +48,7 @@ namespace Barotrauma
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
if (npc.Removed) { continue; }
if (npc.AIController is not HumanAIController humanAiController) { continue; }
if (Operate)
@@ -27,6 +27,7 @@ namespace Barotrauma
foreach (var npc in affectedNpcs)
{
if (npc.Removed) { continue; }
if (npc.AIController is not HumanAIController humanAiController) { continue; }
if (Wait)
@@ -0,0 +1,42 @@
#nullable enable
namespace Barotrauma
{
class OnRoundEndAction : EventAction
{
private readonly SubactionGroup subActions;
public OnRoundEndAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
subActions = new SubactionGroup(parentEvent, element);
}
public override bool IsFinished(ref string goToLabel)
{
return false;
}
public override void Update(float deltaTime)
{
int remainingTries = 100;
string? throwaway = null;
//normally the ref string goTo passed to IsFinished should be used to jump another place in the event,
//but in this case we don't want that (the subactions should just run once when the round ends)
while (remainingTries > 0 && !subActions.IsFinished(ref throwaway))
{
subActions.Update(deltaTime);
Entity.Spawner?.Update(createNetworkEvents: false);
remainingTries--;
}
}
public override void Reset()
{
}
public override string ToDebugString()
{
return nameof(OnRoundEndAction);
}
}
}
@@ -0,0 +1,48 @@
#nullable enable
namespace Barotrauma
{
class SetTraitorEventStateAction : EventAction
{
private readonly TraitorEvent? traitorEvent;
public SetTraitorEventStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (parentEvent is TraitorEvent traitorEvent)
{
this.traitorEvent = traitorEvent;
}
else
{
DebugConsole.ThrowError($"Cannot use the action {nameof(SetTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.");
}
}
[Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)]
public TraitorEvent.State State { get; set; }
private bool isFinished;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished || traitorEvent == null) { return; }
traitorEvent.CurrentState = State;
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetTraitorEventStateAction)} -> (State: {State})";
}
}
}
@@ -16,7 +16,8 @@ namespace Barotrauma
MainPath,
Ruin,
Wreck,
BeaconStation
BeaconStation,
NearMainSub
}
[Serialize("", IsPropertySaveable.Yes, description: "Species name of the character to spawn.")]
@@ -60,6 +61,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes, description: "If false, we won't spawn another character if one with the same identifier has already been spawned.")]
public bool AllowDuplicates { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int Amount { get; set; }
[Serialize(100.0f, IsPropertySaveable.Yes)]
public float Offset { get; set; }
@@ -84,6 +88,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")]
public bool IgnoreByAI { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "If disabled, the action will choose a spawn position away from players' views if one is available.")]
public bool AllowInPlayerView { get; set; }
private bool spawned;
private Entity spawnedEntity;
@@ -159,40 +166,43 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
for (int i = 0; i < Amount; i++)
{
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = TeamID;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
if (LootingIsStealing)
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
{
foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true))
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = TeamID;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
if (LootingIsStealing)
{
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true))
{
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
}
}
}
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier);
foreach (Identifier tag in humanPrefab.GetTags())
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
if (!TargetTag.IsEmpty && newCharacter != null)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag);
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier);
foreach (Identifier tag in humanPrefab.GetTags())
{
outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag);
}
}
}
#if SERVER
newCharacter.LoadTalents();
GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData());
newCharacter.LoadTalents();
GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData());
#endif
});
});
}
}
}
}
@@ -206,14 +216,17 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter =>
for (int i = 0; i < Amount; i++)
{
if (!TargetTag.IsEmpty && newCharacter != null)
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter =>
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
}
}
}
else if (!ItemIdentifier.IsEmpty)
@@ -243,7 +256,7 @@ namespace Barotrauma
if (spawnInventory == null)
{
DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\"");
DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.");
}
}
@@ -252,12 +265,19 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
for (int i = 0; i < Amount; i++)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
}
}
}
else
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned);
for (int i = 0; i < Amount; i++)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned);
}
}
void onSpawned(Item newItem)
{
@@ -298,18 +318,27 @@ namespace Barotrauma
{
if (!SpawnPointTag.IsEmpty)
{
List<Item> potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine));
IEnumerable<Item> potentialItems = Item.ItemList.Where(it => IsValidSubmarineType(SpawnLocation, it.Submarine));
if (!AllowInPlayerView)
{
potentialItems = GetEntitiesNotInPlayerView(potentialItems);
}
var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced();
if (item != null) { return item; }
var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced();
var potentialTargets = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine));
if (!AllowInPlayerView)
{
potentialTargets = GetEntitiesNotInPlayerView(potentialTargets);
}
var target = potentialTargets.GetRandomUnsynced();
if (target != null) { return target; }
}
SpawnType? spawnPointType = null;
if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag);
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView);
}
private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine)
@@ -318,16 +347,39 @@ namespace Barotrauma
{
SpawnLocationType.Any => true,
SpawnLocationType.MainSub => submarine == Submarine.MainSub,
SpawnLocationType.NearMainSub => submarine == null,
SpawnLocationType.MainPath => submarine == null,
SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } },
SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } },
SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } },
SpawnLocationType.Outpost => submarine is { Info.IsOutpost: true },
SpawnLocationType.Wreck => submarine is { Info.IsWreck: true },
SpawnLocationType.Ruin => submarine is { Info.IsRuin: true },
SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null,
_ => throw new NotImplementedException(),
};
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false)
/// <summary>
/// Returns those of the entities that aren't in any player's view. If there are none, all the entities are returned.
/// </summary>
private static IEnumerable<T> GetEntitiesNotInPlayerView<T>(IEnumerable<T> entities) where T : ISpatialEntity
{
if (entities.Any(e => !IsInPlayerView(e)))
{
return entities.Where(e => !IsInPlayerView(e));
}
return entities;
}
private static bool IsInPlayerView(ISpatialEntity entity)
{
foreach (var character in Character.CharacterList)
{
if (!character.IsPlayer || character.IsDead) { continue; }
if (character.CanSeeTarget(entity)) { return true; }
}
return false;
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false, bool allowInPlayerView = true)
{
bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost;
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
@@ -385,6 +437,12 @@ namespace Barotrauma
return potentialSpawnPoints.GetRandomUnsynced();
}
if (spawnLocation == SpawnLocationType.MainPath || spawnLocation == SpawnLocationType.NearMainSub)
{
validSpawnPoints = validSpawnPoints.Where(p =>
Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition)));
}
//avoid using waypoints if there's any actual spawnpoints available
if (validSpawnPoints.Any(wp => wp.SpawnType != SpawnType.Path))
{
@@ -401,7 +459,27 @@ namespace Barotrauma
}
}
if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any())
if (!allowInPlayerView)
{
validSpawnPoints = GetEntitiesNotInPlayerView(validSpawnPoints);
}
if (spawnLocation == SpawnLocationType.NearMainSub && Submarine.MainSub != null)
{
WayPoint closestPoint = validSpawnPoints.First();
float closestDist = float.PositiveInfinity;
foreach (WayPoint wp in validSpawnPoints)
{
float dist = Vector2.DistanceSquared(wp.WorldPosition, Submarine.MainSub.WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
closestPoint = wp;
}
}
return closestPoint;
}
else if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any())
{
WayPoint furthestPoint = validSpawnPoints.First();
float furthestDist = 0.0f;
@@ -1,5 +1,6 @@
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -24,14 +25,25 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowHiddenItems { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool ChooseRandom { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "If larger than 0, the specified percentage of the matching targets are tagged. Between 0-100.")]
public float ChoosePercentage { get; set; }
private bool isFinished = false;
private bool targetNotFound = false;
public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
Taggers = new (string k, Action<Identifier> v)[]
{
("players", v => TagPlayers()),
("player", v => TagPlayers()),
("traitor", v => TagTraitors()),
("nontraitor", v => TagNonTraitors()),
("nontraitorplayer", v => TagNonTraitorPlayers()),
("bot", v => TagBots(playerCrewOnly: false)),
("crew", v => TagCrew()),
("humanprefabidentifier", TagHumansByIdentifier),
@@ -40,8 +52,10 @@ namespace Barotrauma
("structurespecialtag", TagStructuresBySpecialTag),
("itemidentifier", TagItemsByIdentifier),
("itemtag", TagItemsByTag),
("hull", v => TagHulls()),
("hullname", TagHullsByName),
("submarine", TagSubmarinesByType),
("eventtag", TagByEventTag),
}.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary();
}
@@ -54,34 +68,44 @@ namespace Barotrauma
isFinished = false;
}
private void TagByEventTag(Identifier eventTag)
{
AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => SubmarineTypeMatches(t.Submarine)));
}
private void TagPlayers()
{
if (IgnoreIncapacitatedCharacters)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && !c.IsIncapacitated);
}
else
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer);
}
AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters));
}
private void TagTraitors()
{
AddTargetPredicate(Tags.Traitor, e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
}
private void TagNonTraitors()
{
AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
}
private void TagNonTraitorPlayers()
{
AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
}
private void TagBots(bool playerCrewOnly)
{
if (IgnoreIncapacitatedCharacters)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
}
else
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
}
AddTargetPredicate(Tag, e =>
e is Character c &&
c.IsBot &&
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
}
private void TagCrew()
{
#if CLIENT
GameMain.GameSession.CrewManager.GetCharacters().ForEach(c => ParentEvent.AddTarget(Tag, c));
AddTarget(Tag, GameMain.GameSession.CrewManager.GetCharacters());
#else
TagPlayers();
TagBots(playerCrewOnly: true);
@@ -90,54 +114,47 @@ namespace Barotrauma
private void TagHumansByIdentifier(Identifier identifier)
{
foreach (Character c in Character.CharacterList)
{
if (c.HumanPrefab?.Identifier == identifier)
{
ParentEvent.AddTarget(Tag, c);
}
}
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier));
}
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
{
foreach (Character c in Character.CharacterList)
{
if (c.HasJob(jobIdentifier))
{
ParentEvent.AddTarget(Tag, c);
}
}
AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier)));
}
private void TagStructuresByIdentifier(Identifier identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
}
private void TagStructuresBySpecialTag(Identifier tag)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag);
AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag);
}
private void TagItemsByIdentifier(Identifier identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier);
AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier);
}
private void TagItemsByTag(Identifier tag)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag));
AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag));
}
private void TagHulls()
{
AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine));
}
private void TagHullsByName(Identifier name)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
}
private void TagSubmarinesByType(Identifier type)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
}
private bool IsValidItem(Item it)
@@ -152,7 +169,7 @@ namespace Barotrauma
switch (sub.Info.Type)
{
case Barotrauma.SubmarineType.Player:
return SubmarineType.HasFlag(SubType.Player);
return SubmarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle;
case Barotrauma.SubmarineType.Outpost:
case Barotrauma.SubmarineType.OutpostModule:
return SubmarineType.HasFlag(SubType.Outpost);
@@ -165,14 +182,86 @@ namespace Barotrauma
}
}
private void AddTargetPredicate(Identifier tag, Predicate<Entity> predicate)
{
if (ChoosePercentage > 0.0f)
{
TagPercentage(tag, Entity.GetEntities().Where(e => predicate(e)));
}
else if (ChooseRandom)
{
TagRandom(tag, Entity.GetEntities().Where(e => predicate(e)));
}
else
{
ParentEvent.AddTargetPredicate(tag, predicate);
}
}
private void AddTarget(Identifier tag, IEnumerable<Entity> entities)
{
if (entities.None())
{
targetNotFound = true;
return;
}
if (ChoosePercentage > 0.0f)
{
TagPercentage(tag, entities);
}
else if (ChooseRandom)
{
TagRandom(tag, entities);
}
else
{
foreach (var entity in entities)
{
ParentEvent.AddTarget(tag, entity);
}
}
}
private List<Entity> tempEntities;
private void TagPercentage(Identifier tag, IEnumerable<Entity> entities)
{
if (entities.None())
{
targetNotFound = true;
return;
}
int amountToChoose = (int)Math.Ceiling(entities.Count() * (ChoosePercentage / 100.0f));
tempEntities ??= new List<Entity>();
tempEntities.Clear();
for (int i = 0; i < amountToChoose; i++)
{
var entity = entities.GetRandomUnsynced();
tempEntities.Remove(entity);
ParentEvent.AddTarget(tag, entity);
}
}
private void TagRandom(Identifier tag, IEnumerable<Entity> entities)
{
if (entities.None())
{
targetNotFound = true;
return;
}
ParentEvent.AddTarget(tag, entities.GetRandomUnsynced());
}
private readonly ImmutableDictionary<Identifier, Action<Identifier>> Taggers;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (isFinished || targetNotFound) { return; }
string[] criteriaSplit = Criteria.Split(';');
targetNotFound = false;
foreach (string entry in criteriaSplit)
{
string[] kvp = entry.Split(':');
@@ -190,7 +279,7 @@ namespace Barotrauma
}
}
isFinished = true;
isFinished = !targetNotFound;
}
public override string ToDebugString()
@@ -2,7 +2,6 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -50,6 +49,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")]
public bool CheckAllTargets { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If true, interacting with the target will make the character select it.")]
public bool SelectOnTrigger { get; set; }
private float distance;
public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -186,7 +188,12 @@ namespace Barotrauma
{
if (npc != null)
{
if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
if (npc.CampaignInteractionType == CampaignMode.InteractionType.Talk)
{
//if the NPC has a conversation available, don't assign the trigger until the conversation is done
continue;
}
else if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
{
if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc))
{
@@ -213,7 +220,8 @@ namespace Barotrauma
{
npcsOrItems.Add(item);
}
item.CampaignInteractionType = CampaignMode.InteractionType.Examine;
item.AssignCampaignInteractionType(CampaignMode.InteractionType.Examine,
GameMain.NetworkMember?.ConnectedClients.Where(c => c.Character != null && targets2.Contains(c.Character)));
if (player.SelectedItem == item ||
player.SelectedSecondaryItem == item ||
(player.Inventory != null && player.Inventory.Contains(item)) ||
@@ -276,7 +284,7 @@ namespace Barotrauma
}
else if (npcOrItem.TryGet(out Item item))
{
item.CampaignInteractionType = CampaignMode.InteractionType.None;
item.AssignCampaignInteractionType(CampaignMode.InteractionType.None);
}
}
}
@@ -352,6 +360,37 @@ namespace Barotrauma
ParentEvent.AddTarget(ApplyToTarget2, entity2);
}
Character player = null;
Entity target = null;
if (entity1 is Character { IsPlayer: true })
{
player = entity1 as Character;
target = entity2;
}
else if (entity2 is Character { IsPlayer: true })
{
player = entity2 as Character;
target = entity1;
}
if (player != null && SelectOnTrigger)
{
if (target is Character targetCharacter)
{
player.SelectCharacter(targetCharacter);
}
else if (target is Item targetItem)
{
if (targetItem.IsSecondaryItem)
{
player.SelectedSecondaryItem = targetItem;
}
else
{
player.SelectedItem = targetItem;
}
}
}
isRunning = false;
isFinished = true;
}
@@ -10,7 +10,13 @@ partial class TutorialHighlightAction : EventAction
private bool isFinished;
public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (GameMain.NetworkMember != null)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer.");
}
}
public override void Update(float deltaTime)
{
@@ -0,0 +1,75 @@
#nullable enable
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
{
class WaitForItemFabricatedAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CharacterTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the fabricated item(s).")]
public Identifier ApplyTagToItem { get; set; }
private int counter;
public WaitForItemFabricatedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (ItemTag.IsEmpty && ItemIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(WaitForItemFabricatedAction)} does't define either a tag or an identifier of the item to check.");
}
foreach (var item in Item.ItemList)
{
var fabricator = item.GetComponent<Fabricator>();
if (fabricator != null)
{
fabricator.OnItemFabricated += OnItemFabricated;
}
}
}
public void OnItemFabricated(Item item, Character character)
{
if (item == null) { return; }
if (!CharacterTag.IsEmpty)
{
if (!ParentEvent.GetTargets(CharacterTag).Contains(character)) { return; }
}
if (item.ContainerIdentifier == ItemTag || item.HasTag(ItemTag))
{
if (!ApplyTagToItem.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToItem, item);
}
counter++;
}
}
public override bool IsFinished(ref string goTo)
{
return counter >= Amount;
}
public override void Reset()
{
counter = 0;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(counter >= Amount)} {nameof(WaitForItemFabricatedAction)} -> ({ItemTag}, {counter}/{Amount})";
}
}
}
@@ -0,0 +1,153 @@
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class WaitForItemUsedAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier UserTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetItemComponent { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target item when it's used.")]
public Identifier ApplyTagToItem { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the user when the target item is used.")]
public Identifier ApplyTagToUser{ get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")]
public Identifier ApplyTagToHull { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")]
public Identifier ApplyTagToLinkedHulls { get; set; }
private bool isFinished;
private readonly HashSet<Entity> targets = new HashSet<Entity>();
private readonly HashSet<ItemComponent> targetComponents = new HashSet<ItemComponent>();
private Identifier onUseEventIdentifier;
private Identifier OnUseEventIdentifier
{
get
{
if (onUseEventIdentifier.IsEmpty)
{
onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString()).ToIdentifier();
}
return onUseEventIdentifier;
}
}
public WaitForItemUsedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (ItemTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(ItemTag)} not set in {nameof(WaitForItemUsedAction)}.");
}
}
private void OnItemUsed(Item item, Character user)
{
if (!ApplyTagToItem.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToItem, item);
}
if (!ApplyTagToUser.IsEmpty && user != null)
{
ParentEvent.AddTarget(ApplyTagToUser, user);
}
if (item.CurrentHull != null)
{
if (!ApplyTagToHull.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToHull, item.CurrentHull);
}
if (!ApplyTagToLinkedHulls.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToLinkedHulls, item.CurrentHull);
foreach (var linkedHull in item.CurrentHull.GetLinkedEntities<Hull>())
{
ParentEvent.AddTarget(ApplyTagToLinkedHulls, linkedHull);
}
}
}
DeregisterTargets();
isFinished = true;
}
public override void Update(float deltaTime)
{
TryRegisterTargets();
}
private void TryRegisterTargets()
{
foreach (Entity target in ParentEvent.GetTargets(ItemTag))
{
//already registered, ignore
if (targets.Contains(target)) { continue; }
if (target is not Item item) { continue; }
if (TargetItemComponent.IsEmpty)
{
item.GetComponents<ItemComponent>().ForEach(ic => Register(ic));
}
else if (item.Components.FirstOrDefault(ic => ic.Name == TargetItemComponent) is ItemComponent targetItemComponent)
{
Register(targetItemComponent);
}
else
{
#if DEBUG
DebugConsole.ThrowError($"Failed to find the component {TargetItemComponent} on item {item.Prefab.Identifier}");
#endif
}
}
void Register(ItemComponent ic)
{
targets.Add(ic.Item);
targetComponents.Add(ic);
ic.OnUsed.RegisterOverwriteExisting(
OnUseEventIdentifier,
i => { OnItemUsed(i.Item, i.User); });
}
}
private void DeregisterTargets()
{
foreach (ItemComponent ic in targetComponents)
{
ic.OnUsed.Deregister(OnUseEventIdentifier);
}
targetComponents.Clear();
targets.Clear();
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
DeregisterTargets();
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(WaitForItemUsedAction)} -> ({ItemTag})";
}
}
}