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
@@ -64,7 +64,16 @@ namespace Barotrauma
spawnPending = true;
}
public override string GetDebugInfo()
{
return
$"Finished: {IsFinished.ColorizeObject()}\n" +
$"Item: {Item.ColorizeObject()}\n" +
$"Spawn pending: {SpawnPending.ColorizeObject()}\n" +
$"Spawn position: {SpawnPos.ColorizeObject()}";
}
private void SpawnItem()
{
item = new Item(itemPrefab, spawnPos, null);
@@ -73,7 +82,7 @@ namespace Barotrauma
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
{
if (it.Submarine != null || !it.HasTag("artifactholder")) continue;
if (it.Submarine != null || !it.HasTag(Tags.ArtifactHolder)) { continue; }
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) continue;
@@ -52,6 +52,11 @@ namespace Barotrauma
ParentSet = parentSet;
}
public virtual string GetDebugInfo()
{
return $"Finished: {IsFinished.ColorizeObject()}";
}
public virtual void Update(float deltaTime)
{
}
@@ -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})";
}
}
}
@@ -0,0 +1,65 @@
#nullable enable
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Used to store logs of scripted events (a sort of "quest log")
/// </summary>
partial class EventLog
{
public class Event
{
public readonly Identifier EventIdentifier;
public readonly List<Entry> Entries = new List<Entry>();
public Event(Identifier eventPrefabId)
{
EventIdentifier = eventPrefabId;
}
}
public class Entry
{
public readonly Identifier Identifier;
public string Text;
public Entry(Identifier identifier, string text)
{
Identifier = identifier;
Text = text;
}
}
private readonly Dictionary<Identifier, Event> events = new Dictionary<Identifier, Event>();
private bool TryAddEntryInternal(Identifier eventPrefabId, Identifier entryId, string text)
{
if (!events.TryGetValue(eventPrefabId, out Event? ev))
{
ev = new Event(eventPrefabId);
events.Add(eventPrefabId, ev);
}
Entry? entry = ev.Entries.FirstOrDefault(e => e.Identifier == entryId);
if (entry == null)
{
ev.Entries.Add(new Entry(entryId, text));
return true;
}
else if (entry.Text != text)
{
entry.Text = text;
return true;
}
return false;
}
public void Clear()
{
events.Clear();
}
}
}
@@ -17,9 +17,24 @@ namespace Barotrauma
CONVERSATION_SELECTED_OPTION,
STATUSEFFECT,
MISSION,
UNLOCKPATH
UNLOCKPATH,
EVENTLOG,
EVENTOBJECTIVE,
}
[NetworkSerialize]
public readonly record struct NetEventLogEntry(Identifier EventPrefabId, Identifier LogEntryId, string Text) : INetSerializableStruct;
[NetworkSerialize]
public readonly record struct NetEventObjective(
EventObjectiveAction.SegmentActionType Type,
Identifier Identifier,
Identifier ObjectiveTag,
Identifier TextTag,
Identifier ParentObjectiveId,
bool CanBeCompleted) : INetSerializableStruct;
const float IntensityUpdateInterval = 5.0f;
const float CalculateDistanceTraveledInterval = 5.0f;
@@ -95,7 +110,7 @@ namespace Barotrauma
get { return musicIntensity; }
}
public List<Event> ActiveEvents
public IEnumerable<Event> ActiveEvents
{
get { return activeEvents; }
}
@@ -119,6 +134,8 @@ namespace Barotrauma
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
public readonly EventLog EventLog = new EventLog();
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
@@ -198,7 +215,7 @@ namespace Barotrauma
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
ActiveEvents.Add(newEvent);
activeEvents.Add(newEvent);
}
else
{
@@ -256,7 +273,18 @@ namespace Barotrauma
CumulativeMonsterStrengthWrecks = 0;
CumulativeMonsterStrengthCaves = 0;
}
public void ActivateEvent(Event newEvent)
{
activeEvents.Add(newEvent);
newEvent.Init();
}
public void ClearEvents()
{
activeEvents.Clear();
}
private void SelectSettings()
{
if (!EventManagerSettings.Prefabs.Any())
@@ -364,6 +392,14 @@ namespace Barotrauma
}
}
public void TriggerOnEndRoundActions()
{
foreach (var ev in activeEvents)
{
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
}
}
public void EndRound()
{
pendingEventSets.Clear();
@@ -478,14 +514,6 @@ namespace Barotrauma
}
}
bool isPrefabSuitable(EventPrefab e) =>
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
!level.LevelData.NonRepeatableEvents.Contains(e.Identifier) &&
isFactionSuitable(e.Faction);
bool isFactionSuitable(Identifier factionId) =>
factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier;
foreach (var subEventPrefab in eventSet.EventPrefabs)
{
foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers())
@@ -495,7 +523,7 @@ namespace Barotrauma
}
var suitablePrefabSubsets = eventSet.EventPrefabs.Where(
e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray();
e => IsFactionSuitable(e.Faction, level) && e.EventPrefabs.Any(ep => IsSuitable(ep, level))).ToArray();
for (int i = 0; i < applyCount; i++)
{
@@ -512,7 +540,7 @@ namespace Barotrauma
(IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) = subEventPrefab;
if (eventPrefabs != null && random.NextDouble() <= probability)
{
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.RandomSeed = randomSeed;
@@ -529,10 +557,25 @@ namespace Barotrauma
}
if (eventSet.ChildSets.Any())
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random);
if (newEventSet != null)
int setCount = eventSet.SubSetCount;
if (setCount > 1)
{
CreateEvents(newEventSet);
var unusedSets = eventSet.ChildSets.ToList();
for (int j = 0; j < setCount; j++)
{
var newEventSet = SelectRandomEvents(unusedSets, random: random);
if (newEventSet == null) { break; }
unusedSets.Remove(newEventSet);
CreateEvents(newEventSet);
}
}
else
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random);
if (newEventSet != null)
{
CreateEvents(newEventSet);
}
}
}
}
@@ -542,7 +585,7 @@ namespace Barotrauma
{
if (random.NextDouble() > probability) { continue; }
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
if (!selectedEvents.ContainsKey(eventSet))
@@ -636,6 +679,23 @@ namespace Barotrauma
return null;
}
public static bool IsSuitable(EventPrefab e, Level level)
{
return IsLevelSuitable(e, level) && IsFactionSuitable(e.Faction, level);
}
public static bool IsLevelSuitable(EventPrefab e, Level level)
{
return
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
!level.LevelData.NonRepeatableEvents.Contains(e.Identifier);
}
private static bool IsFactionSuitable(Identifier factionId, Level level)
{
return factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier;
}
private static bool IsValidForLevel(EventSet eventSet, Level level)
{
return
@@ -1031,35 +1091,6 @@ namespace Barotrauma
}
}
/// <summary>
/// Finds all actions in a ScriptedEvent
/// </summary>
private static List<Tuple<int, EventAction>> FindActions(ScriptedEvent scriptedEvent)
{
var list = new List<Tuple<int, EventAction>>();
foreach (EventAction eventAction in scriptedEvent.Actions)
{
list.AddRange(FindActionsRecursive(eventAction));
}
return list;
static List<Tuple<int, EventAction>> FindActionsRecursive(EventAction eventAction, int ident = 1)
{
var eventActions = new List<Tuple<int, EventAction>> { Tuple.Create(ident, eventAction) };
ident++;
foreach (var action in eventAction.GetSubActions())
{
eventActions.AddRange(FindActionsRecursive(action, ident));
}
return eventActions;
}
}
/// <summary>
/// Get the entity that should be used in determining how far the player has progressed in the level.
/// = The submarine or player character that has progressed the furthest.
@@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace Barotrauma
{
@@ -16,12 +15,25 @@ namespace Barotrauma
public readonly float Commonness;
public readonly Identifier BiomeIdentifier;
public readonly Identifier Faction;
public readonly float SpawnDistance;
public readonly LocalizedString Name;
public readonly bool UnlockPathEvent;
public readonly string UnlockPathTooltip;
public readonly int UnlockPathReputation;
public static EventPrefab Create(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default)
{
if (element.NameAsIdentifier() == nameof(TraitorEvent))
{
return new TraitorEventPrefab(element, file, fallbackIdentifier);
}
else
{
return new EventPrefab(element, file, fallbackIdentifier);
}
}
public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default)
: base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier))
{
@@ -40,6 +52,8 @@ namespace Barotrauma
DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".");
}
Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString());
BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty);
Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
@@ -49,19 +63,17 @@ namespace Barotrauma
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
SpawnDistance = element.GetAttributeFloat("spawndistance", 0);
}
public bool TryCreateInstance<T>(out T instance) where T : Event
{
instance = CreateInstance() as T;
return instance is T;
return instance is not null;
}
public Event CreateInstance()
{
ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(EventPrefab) });
ConstructorInfo constructor = EventType.GetConstructor(new[] { GetType() });
Event instance = null;
try
{
@@ -79,7 +91,7 @@ namespace Barotrauma
public override string ToString()
{
return $"EventPrefab ({Identifier})";
return $"{nameof(EventPrefab)} ({Identifier})";
}
public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction)
@@ -94,6 +94,7 @@ namespace Barotrauma
public readonly bool ChooseRandom;
private readonly int eventCount = 1;
public readonly int SubSetCount = 1;
private readonly Dictionary<Identifier, int> overrideEventCount = new Dictionary<Identifier, int>();
/// <summary>
@@ -280,6 +281,7 @@ namespace Barotrauma
ChooseRandom = element.GetAttributeBool("chooserandom", false);
eventCount = element.GetAttributeInt("eventcount", 1);
SubSetCount = element.GetAttributeInt("setcount", 1);
Exhaustible = element.GetAttributeBool("exhaustible", false);
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
@@ -295,7 +297,7 @@ namespace Barotrauma
OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false));
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
ResetTime = element.GetAttributeFloat("resettime", 0);
ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0);
CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false);
ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1);
@@ -474,7 +476,7 @@ namespace Barotrauma
if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent))
{
if (filter != null && !filter(monsterEvent)) { return; }
float spawnProbability = monsterEvent.Prefab.Probability;
float spawnProbability = monsterEvent.Prefab?.Probability ?? 0.0f;
if (Rand.Value() > spawnProbability) { return; }
int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1);
if (count <= 0) { return; }
@@ -16,7 +16,7 @@ namespace Barotrauma
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
private readonly string itemTag;
private readonly Identifier itemTag;
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
@@ -90,7 +90,7 @@ namespace Barotrauma
hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag);
itemConfig = prefab.ConfigElement.GetChildElement("Items");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
itemTag = prefab.ConfigElement.GetAttributeIdentifier("targetitem", Identifier.Empty);
}
protected override void StartMissionSpecific(Level level)
@@ -118,7 +118,7 @@ namespace Barotrauma
private void InitItems(Submarine submarine)
{
if (!string.IsNullOrEmpty(itemTag))
if (!itemTag.IsEmpty)
{
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (!itemsToDestroy.Any())
@@ -73,7 +73,7 @@ namespace Barotrauma
{
get
{
if (level.BeaconStation == null)
if (level.BeaconStation == null || state > 0)
{
yield break;
}
@@ -95,9 +95,10 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; }
if (item.GetComponent<PowerTransfer>() != null ||
bool isReactor = item.GetComponent<Reactor>() != null;
if ((isReactor && GameMain.GameSession is not { TraitorsEnabled: true }) ||
item.GetComponent<PowerTransfer>() != null ||
item.GetComponent<PowerContainer>() != null ||
item.GetComponent<Reactor>() != null ||
item.GetComponent<Sonar>() != null)
{
item.InvulnerableToDamage = true;
@@ -262,6 +262,12 @@ namespace Barotrauma
SpawnedInCurrentOutpost = true,
AllowStealing = false
};
item.AddTag("cargomission");
item.AddTag(Prefab.Identifier);
foreach (var tag in Prefab.Tags)
{
item.AddTag(tag);
}
item.FindHull();
items.Add(item);
@@ -23,6 +23,7 @@ namespace Barotrauma
private readonly CharacterPrefab minionPrefab;
private readonly Identifier spawnPointTag;
private WayPoint bossSpawnPoint;
private readonly Identifier destructibleItemTag;
private readonly string endCinematicSound;
@@ -68,7 +69,13 @@ namespace Barotrauma
{
if (boss != null && !boss.Removed)
{
Vector2 prevPos = boss.AnimController.Collider.SimPosition;
boss.AnimController.ColliderIndex = 1;
if (bossSpawnPoint != null)
{
//ensure the new collider stays in the same position (the 2nd one has a different shape than the 1st one)
boss.AnimController.Collider.SetTransform(prevPos, 0.0f);
}
}
}, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2);
}
@@ -142,21 +149,21 @@ namespace Barotrauma
protected override void StartMissionSpecific(Level level)
{
var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag));
if (spawnPoint == null)
bossSpawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag));
if (bossSpawnPoint == null)
{
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\".");
return;
}
if (!IsClient)
{
boss = Character.Create(bossPrefab.Identifier, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
boss = Character.Create(bossPrefab.Identifier, bossSpawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
var minionList = new List<Character>();
float angle = 0;
float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1);
for (int i = 0; i < minionCount; i++)
{
minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(spawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false));
minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(bossSpawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false));
angle += angleStep;
}
SwarmBehavior.CreateSwarm(minionList.Cast<AICharacter>());
@@ -315,27 +315,21 @@ namespace Barotrauma
}
}
private bool Survived(Character character)
private static bool Survived(Character character)
{
return IsAlive(character) && character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine));
}
private bool IsAlive(Character character)
private static bool IsAlive(Character character)
{
return character != null && !character.Removed && !character.IsDead;
}
private bool IsCaptured(Character character)
{
return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier);
}
protected override bool DetermineCompleted()
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c));
bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c));
bool vipDied = false;
@@ -345,7 +339,7 @@ namespace Barotrauma
vipDied = !Survived(vipCharacter);
}
if (friendliesSurvived && !terroristsSurvived && !vipDied)
if (friendliesSurvived && !vipDied)
{
return true;
}
@@ -139,7 +139,7 @@ namespace Barotrauma
{
if (cave.Area.Contains(spawnedResource.WorldPosition))
{
cave.DisplayOnSonar = true;
cave.MissionsToDisplayOnSonar.Add(this);
caves.Add(cave);
break;
}
@@ -95,7 +95,7 @@ namespace Barotrauma
}
}
public Dictionary<Identifier, float> ReputationRewards
public ImmutableList<MissionPrefab.ReputationReward> ReputationRewards
{
get { return Prefab.ReputationRewards; }
}
@@ -268,7 +268,7 @@ namespace Barotrauma
delayedTriggerEvents.Clear();
foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
{
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false))
foreach (MapEntity entityToShow in MapEntity.MapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false))
{
entityToShow.HiddenInGame = false;
}
@@ -353,8 +353,7 @@ namespace Barotrauma
if (GameMain.GameSession?.EventManager != null)
{
var newEvent = eventPrefab.CreateInstance();
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init();
GameMain.GameSession.EventManager.ActivateEvent(newEvent);
}
}
@@ -372,7 +371,19 @@ namespace Barotrauma
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
try
{
GiveReward();
}
catch (Exception e)
{
string errorMsg = "Unknown error while giving mission rewards.";
DebugConsole.ThrowError(errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("Mission.End:GiveReward", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace);
#if SERVER
GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error);
#endif
}
}
TimesAttempted++;
@@ -423,30 +434,7 @@ namespace Barotrauma
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
int experienceGain = (int)(baseExperienceGain * experienceGainMultiplier.Value);
#if CLIENT
foreach (Character character in crewCharacters)
{
GiveMissionExperience(character.Info);
}
#else
foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients)
{
//give the experience to the stored characterinfo if the client isn't currently controlling a character
GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo);
}
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
{
GiveMissionExperience(bot.Info);
}
#endif
void GiveMissionExperience(CharacterInfo info)
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value));
}
DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value));
CalculateFinalReward(Submarine.MainSub);
#if SERVER
@@ -465,17 +453,32 @@ namespace Barotrauma
character.Info.MissionsCompletedSinceDeath++;
}
foreach (KeyValuePair<Identifier, float> reputationReward in ReputationRewards)
foreach (var reputationReward in ReputationRewards)
{
if (reputationReward.Key == "location")
if (reputationReward.FactionIdentifier == "location")
{
OriginLocation.Reputation?.AddReputation(reputationReward.Value);
OriginLocation.Reputation?.AddReputation(reputationReward.Amount);
TryGiveReputationForOpposingFaction(OriginLocation.Faction, reputationReward.AmountForOpposingFaction);
}
else
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key);
float prevValue = faction.Reputation.Value;
faction?.Reputation.AddReputation(reputationReward.Value);
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.FactionIdentifier);
if (faction != null)
{
faction.Reputation.AddReputation(reputationReward.Amount);
TryGiveReputationForOpposingFaction(faction, reputationReward.AmountForOpposingFaction);
}
}
}
void TryGiveReputationForOpposingFaction(Faction thisFaction, float amount)
{
if (MathUtils.NearlyEqual(amount, 0.0f)) { return; }
if (thisFaction?.Prefab != null &&
!thisFaction.Prefab.OpposingFaction.IsEmpty)
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == thisFaction.Prefab.OpposingFaction);
faction?.Reputation.AddReputation(amount);
}
}
}
@@ -489,30 +492,10 @@ namespace Barotrauma
}
}
#if SERVER
public static int DistributeRewardsToCrew(IEnumerable<Character> crew, int totalReward)
{
int remainingRewards = totalReward;
float sum = GetRewardDistibutionSum(crew);
if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; }
foreach (Character character in crew)
{
int rewardDistribution = character.Wallet.RewardDistribution;
float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f;
int reward = (int)(totalReward * rewardWeight);
reward = Math.Min(remainingRewards, reward);
character.Wallet.Give(reward);
remainingRewards -= reward;
if (remainingRewards <= 0) { break; }
}
return remainingRewards;
}
#endif
partial void DistributeExperienceToCrew(IEnumerable<Character> crew, int experienceGain);
public static int GetRewardDistibutionSum(IEnumerable<Character> crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution;
public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable<Character> crew, Option<int> reward)
{
float sum = GetRewardDistibutionSum(crew, rewardDistribution);
@@ -4,7 +4,6 @@ using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -54,6 +53,20 @@ namespace Barotrauma
{ MissionType.Combat, typeof(CombatMission) }
};
public class ReputationReward
{
public readonly Identifier FactionIdentifier;
public readonly float Amount;
public readonly float AmountForOpposingFaction;
public ReputationReward(XElement element)
{
FactionIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
Amount = element.GetAttributeFloat(nameof(Amount), 0.0f);
AmountForOpposingFaction = element.GetAttributeFloat(nameof(AmountForOpposingFaction), 0.0f);
}
}
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo, MissionType.End };
private readonly ConstructorInfo constructor;
@@ -75,7 +88,7 @@ namespace Barotrauma
public readonly Identifier AchievementIdentifier;
public readonly Dictionary<Identifier, float> ReputationRewards = new Dictionary<Identifier, float>();
public readonly ImmutableList<ReputationReward> ReputationRewards;
public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>
DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>();
@@ -252,7 +265,8 @@ namespace Barotrauma
messages.Add(message);
}
}
List<ReputationReward> reputationRewards = new List<ReputationReward>();
int messageIndex = 0;
foreach (var subElement in element.Elements())
{
@@ -292,14 +306,7 @@ namespace Barotrauma
break;
case "reputation":
case "reputationreward":
Identifier factionIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
float amount = subElement.GetAttributeFloat("amount", 0.0f);
if (ReputationRewards.ContainsKey(factionIdentifier))
{
DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Multiple reputation changes defined for the identifier \"{factionIdentifier}\".");
continue;
}
ReputationRewards.Add(factionIdentifier, amount);
reputationRewards.Add(new ReputationReward(subElement));
break;
case "metadata":
Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
@@ -325,6 +332,7 @@ namespace Barotrauma
}
Headers = headers.ToImmutableArray();
Messages = messages.ToImmutableArray();
ReputationRewards = reputationRewards.ToImmutableList();
Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty);
//backwards compatibility
@@ -399,7 +407,7 @@ namespace Barotrauma
else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins)
{
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
if (connection?.LevelData == null || connection.LevelData.GenerationParams.RuinCount < 1) { return false; }
if (connection?.LevelData == null || connection.LevelData.GenerationParams.GetMaxRuinCount() < 1) { return false; }
}
return false;
@@ -147,7 +147,7 @@ namespace Barotrauma
monster.Params.AI.FleeHealthThreshold = 0;
foreach (var targetParam in monster.Params.AI.Targets)
{
if (targetParam.Tag.Equals("engine", StringComparison.OrdinalIgnoreCase)) { continue; }
if (targetParam.Tag == "engine") { continue; }
switch (targetParam.State)
{
case AIState.Avoid:
@@ -4,7 +4,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
namespace Barotrauma
@@ -30,6 +29,8 @@ namespace Barotrauma
private Vector2 nestPosition;
private Level.Cave selectedCave;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
@@ -125,11 +126,9 @@ namespace Barotrauma
}
if (closestCave != null)
{
closestCave.DisplayOnSonar = true;
SpawnNestObjects(level, closestCave);
#if SERVER
selectedCave = closestCave;
#endif
selectedCave.MissionsToDisplayOnSonar.Add(this);
SpawnNestObjects(level, closestCave);
}
var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3);
if (nearbyCells.Any())
@@ -172,8 +171,8 @@ namespace Barotrauma
foreach (var subElement in itemConfig.Elements())
{
string itemIdentifier = subElement.GetAttributeString("identifier", "");
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
var itemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab)
{
DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found");
continue;
@@ -183,25 +182,34 @@ namespace Barotrauma
float rotation = 0.0f;
if (spawnEdges.Any())
{
var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient);
spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient));
Vector2 normal = Vector2.UnitY;
if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid)
const float MinDistanceFromOtherItems = 30.0f;
const int MaxTries = 10;
for (int i = 0; i < MaxTries; i++)
{
normal = edge.GetNormal(edge.Cell1);
var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient);
spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient));
Vector2 normal = Vector2.UnitY;
if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid)
{
normal = edge.GetNormal(edge.Cell1);
}
else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid)
{
normal = edge.GetNormal(edge.Cell2);
}
spawnPos += normal * 10.0f;
rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2;
if (items.All(it => Vector2.DistanceSquared(it.WorldPosition, spawnPos) > MinDistanceFromOtherItems)) { break; }
}
else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid)
{
normal = edge.GetNormal(edge.Cell2);
}
spawnPos += normal * 10.0f;
rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2;
}
var item = new Item(itemPrefab, spawnPos, null);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation);
item.FindHull();
item.AddTag("nestmission");
item.AddTag(Prefab.Identifier);
items.Add(item);
var statusEffectElement =
@@ -286,7 +294,10 @@ namespace Barotrauma
}
//continue when all items are in the sub or destroyed
if (AllItemsDestroyedOrRetrieved()) { State = 1; }
if (AllItemsDestroyedOrRetrieved())
{
State = 1;
}
break;
case 1:
@@ -239,7 +239,7 @@ namespace Barotrauma
private void InitPirateShip()
{
enemySub.NeutralizeBallast();
if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag(Tags.Reactor) && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
reactor.PowerUpImmediately();
}
@@ -30,8 +30,8 @@ namespace Barotrauma
public readonly ItemPrefab ItemPrefab;
public readonly Level.PositionType SpawnPositionType;
public readonly string ContainerTag;
public readonly string ExistingItemTag;
public readonly Identifier ContainerTag;
public readonly Identifier ExistingItemTag;
public readonly bool RemoveItem;
@@ -87,7 +87,7 @@ namespace Barotrauma
public Target(ContentXElement element, SalvageMission mission)
{
this.mission = mission;
ContainerTag = element.GetAttributeString("containertag", "");
ContainerTag = element.GetAttributeIdentifier("containertag", Identifier.Empty);
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub);
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false);
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false);
@@ -100,7 +100,7 @@ namespace Barotrauma
.Fallback(TextManager.Get(sonarLabelTag))
.Fallback(element.GetAttributeString("sonarlabel", ""));
}
ExistingItemTag = element.GetAttributeString("existingitemtag", "");
ExistingItemTag = element.GetAttributeIdentifier("existingitemtag", Identifier.Empty);
RemoveItem = element.GetAttributeBool("removeitem", true);
@@ -109,7 +109,7 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
string itemName = element.GetAttributeString("itemname", "");
ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty())
if (ItemPrefab == null && ExistingItemTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\"");
}
@@ -126,7 +126,7 @@ namespace Barotrauma
string itemTag = element.GetAttributeString("itemtag", "");
ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab;
}
if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty())
if (ItemPrefab == null && ExistingItemTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\"");
}
@@ -233,7 +233,7 @@ namespace Barotrauma
Vector2.Zero :
Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f);
if (!string.IsNullOrEmpty(target.ExistingItemTag))
if (!target.ExistingItemTag.IsEmpty)
{
var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag));
if (GameMain.GameSession?.Missions != null)
@@ -284,9 +284,9 @@ namespace Barotrauma
if (target.Item == null)
{
if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag))
if (target.ItemPrefab == null && target.ContainerTag.IsEmpty)
{
DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}");
DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag}");
continue;
}
target.Item = new Item(target.ItemPrefab, position, null);
@@ -312,8 +312,10 @@ namespace Barotrauma
#endif
}
target.Item.IsSalvageMissionItem = true;
//try to find a container and place the item inside it
if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null)
if (!target.ContainerTag.IsEmpty && target.Item.ParentInventory == null)
{
List<ItemContainer> validContainers = new List<ItemContainer>();
foreach (Item it in Item.ItemList)
@@ -218,7 +218,7 @@ namespace Barotrauma
#endif
}
private bool IsValidScanPosition(Scanner scanner, KeyValuePair<WayPoint, bool> scanStatus, float scanRadiusSquared)
private static bool IsValidScanPosition(Scanner scanner, KeyValuePair<WayPoint, bool> scanStatus, float scanRadiusSquared)
{
if (scanStatus.Value) { return false; }
if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; }
@@ -232,39 +232,15 @@ namespace Barotrauma
switch (State)
{
case 0:
if (!AllTargetsScanned) { return; }
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
if (AllTargetsScanned)
{
State = 1;
}
break;
}
}
protected override bool DetermineCompleted()
{
return State == 2 && AllScannersReturned();
bool AllScannersReturned()
{
foreach (var scanner in scanners)
{
if (scanner?.Item == null || scanner.Item.Removed) { return false; }
var owner = scanner.Item.GetRootInventoryOwner();
if (owner.Submarine != null && owner.Submarine.Info.Type == SubmarineType.Player)
{
continue;
}
else if (owner is Character c && c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info))
{
continue;
}
return false;
}
return true;
}
}
protected override bool DetermineCompleted() => State > 0;
protected override void EndMissionSpecific(bool completed)
{
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
namespace Barotrauma
{
@@ -13,6 +14,7 @@ namespace Barotrauma
public readonly int MinAmount, MaxAmount;
private readonly List<Character> monsters = new List<Character>();
public readonly float SpawnDistance;
private readonly float scatter;
private readonly float offset;
private readonly float delayBetweenSpawns;
@@ -56,7 +58,7 @@ namespace Barotrauma
}
public MonsterEvent(EventPrefab prefab)
: base (prefab)
: base(prefab)
{
string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", "");
CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile);
@@ -94,7 +96,7 @@ namespace Barotrauma
}
spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty);
SpawnDistance = prefab.ConfigElement.GetAttributeFloat("spawndistance", 0);
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
@@ -174,6 +176,15 @@ namespace Barotrauma
}
}
public override string GetDebugInfo()
{
return
$"Finished: {IsFinished.ColorizeObject()}\n" +
$"Amount: {MinAmount.ColorizeObject()} - {MaxAmount.ColorizeObject()}\n" +
$"Spawn pending: {SpawnPending.ColorizeObject()}\n" +
$"Spawn position: {SpawnPos.ColorizeObject()}";
}
private List<Level.InterestingPosition> GetAvailableSpawnPositions()
{
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => SpawnPosType.HasFlag(p.PositionType));
@@ -214,13 +225,14 @@ namespace Barotrauma
return availablePositions;
}
private Level.InterestingPosition chosenPosition;
private void FindSpawnPosition(bool affectSubImmediately)
{
if (disallowed) { return; }
spawnPos = Vector2.Zero;
var availablePositions = GetAvailableSpawnPositions();
var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck);
if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
@@ -454,52 +466,109 @@ namespace Barotrauma
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
float minDist = GetMinDistanceToSub(submarine);
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist)
{
// Too close to a player sub.
return;
}
}
}
float minDistance = Prefab.SpawnDistance;
if (minDistance <= 0)
float spawnDistance = SpawnDistance;
if (spawnDistance <= 0)
{
if (SpawnPosType.HasFlag(Level.PositionType.Cave))
{
minDistance = 8000;
spawnDistance = 8000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Ruin))
{
minDistance = 5000;
spawnDistance = 5000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation))
{
minDistance = 3000;
spawnDistance = 3000;
}
}
if (minDistance > 0)
if (spawnDistance > 0)
{
bool someoneNearby = false;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance))
float distanceSquared = Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value);
if (distanceSquared < MathUtils.Pow2(spawnDistance))
{
someoneNearby = true;
break;
if (chosenPosition.Submarine != null)
{
Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine);
Vector2 to = Submarine.GetRelativeSimPositionFromWorldPosition(submarine.WorldPosition, chosenPosition.Submarine, submarine);
if (CheckLineOfSight(from, to, chosenPosition.Submarine))
{
// Line of sight to a player sub -> don't spawn yet.
return;
}
}
else
{
break;
}
}
}
foreach (Character c in Character.CharacterList)
{
if (c == Character.Controlled || c.IsRemotePlayer)
{
if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < MathUtils.Pow2(minDistance))
float distanceSquared = Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value);
if (distanceSquared < MathUtils.Pow2(spawnDistance))
{
someoneNearby = true;
break;
if (chosenPosition.Submarine != null)
{
Vector2 from = Submarine.GetRelativeSimPositionFromWorldPosition(spawnPos.Value, chosenPosition.Submarine, chosenPosition.Submarine);
Vector2 to = Submarine.GetRelativeSimPositionFromWorldPosition(c.WorldPosition, chosenPosition.Submarine, c.Submarine);
if (CheckLineOfSight(from, to, chosenPosition.Submarine))
{
// Line of sight to a player character -> don't spawn. Disable the event to prevent monsters "magically" spawning here.
Finish();
return;
}
}
else
{
break;
}
}
}
}
if (!someoneNearby) { return; }
static bool CheckLineOfSight(Vector2 from, Vector2 to, Submarine targetSub)
{
var bodies = Submarine.PickBodies(from, to, ignoredBodies: null, Physics.CollisionWall);
foreach (var b in bodies)
{
if (b.UserData is ISpatialEntity spatialEntity && spatialEntity.Submarine != targetSub)
{
// Different sub -> ignore
continue;
}
if (b.UserData is Structure s && !s.IsPlatform && s.CastShadow)
{
return false;
}
if (b.UserData is Item item && item.GetComponent<Door>() is Door door)
{
if (!door.IsBroken && !door.IsOpen)
{
return false;
}
}
}
return true;
}
}
if (SpawnPosType.HasFlag(Level.PositionType.Abyss) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave))
{
bool anyInAbyss = false;
@@ -1,4 +1,5 @@
using System;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -9,10 +10,19 @@ namespace Barotrauma
private readonly Dictionary<Identifier, List<Predicate<Entity>>> targetPredicates = new Dictionary<Identifier, List<Predicate<Entity>>>();
private readonly Dictionary<Identifier, List<Entity>> cachedTargets = new Dictionary<Identifier, List<Entity>>();
/// <summary>
/// How many targets were there when they were tagged for the first time? Can be used by some EventActions to check how many entities
/// there are still left (e.g. how much of the initial cargo still exists)
/// </summary>
private readonly Dictionary<Identifier, int> initialAmounts = new Dictionary<Identifier, int>();
private int prevEntityCount;
private int prevPlayerCount, prevBotCount;
private Character prevControlled;
public readonly OnRoundEndAction OnRoundEndAction;
private readonly string[] requiredDestinationTypes;
public readonly bool RequireBeaconStation;
@@ -20,16 +30,25 @@ namespace Barotrauma
public List<EventAction> Actions { get; } = new List<EventAction>();
public Dictionary<Identifier, List<Entity>> Targets { get; } = new Dictionary<Identifier, List<Entity>>();
protected virtual IEnumerable<Identifier> NonActionChildElementNames => Enumerable.Empty<Identifier>();
public override string ToString()
{
return $"ScriptedEvent ({prefab.Identifier})";
return $"{nameof(ScriptedEvent)} ({prefab.Identifier})";
}
public ScriptedEvent(EventPrefab prefab) : base(prefab)
{
foreach (var element in prefab.ConfigElement.Elements())
{
if (element.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
Identifier elementId = element.Name.ToIdentifier();
if (NonActionChildElementNames.Contains(elementId)) { continue; }
if (elementId == nameof(Barotrauma.OnRoundEndAction))
{
OnRoundEndAction = EventAction.Instantiate(this, element) as OnRoundEndAction;
continue;
}
if (elementId == "statuseffect")
{
DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction.");
continue;
@@ -46,31 +65,125 @@ namespace Barotrauma
requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
var allActions = GetAllActions().Select(a => a.action);
foreach (var gotoAction in allActions.OfType<GoTo>())
{
if (allActions.None(a => a is Label label && label.Name == gotoAction.Name))
{
DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\".");
}
}
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start");
}
public override string GetDebugInfo()
{
EventAction currentAction = !IsFinished ? Actions[CurrentActionIndex] : null;
string text = $"Finished: {IsFinished.ColorizeObject()}\n" +
$"Action index: {CurrentActionIndex.ColorizeObject()}\n" +
$"Current action: {currentAction?.ToDebugString() ?? ToolBox.ColorizeObject(null)}\n";
text += "All actions:\n";
text += GetAllActions().Aggregate(string.Empty, (current, action) => current + $"{new string(' ', action.indent * 6)}{action.action.ToDebugString()}\n");
text += "Targets:\n";
foreach (var (key, value) in Targets)
{
text += $" {key.ColorizeObject()}: {value.Aggregate(string.Empty, (current, entity) => current + $"{entity.ColorizeObject()} ")}\n";
}
return text;
}
public virtual string GetTextForReplacementElement(string tag)
{
if (tag.StartsWith("eventtag:"))
{
string targetTag = tag["eventtag:".Length..];
Entity target = GetTargets(targetTag.ToIdentifier()).FirstOrDefault();
if (target != null)
{
if (target is Item item) { return item.Name; }
if (target is Character character) { return character.Name; }
if (target is Hull hull) { return hull.DisplayName.Value; }
if (target is Submarine sub) { return sub.Info.DisplayName.Value; }
DebugConsole.AddWarning($"Failed to get the name of the event target {target} as a replacement for the tag {tag} in an event text.");
return target.ToString();
}
else
{
return $"[target \"{targetTag}\" not found]";
}
}
return string.Empty;
}
public virtual LocalizedString ReplaceVariablesInEventText(LocalizedString str)
{
return str;
}
/// <summary>
/// Finds all actions in the ScriptedEvent (recursively going through the subactions as well).
/// Returns a list of tuples where the first value is the indentation level (or "how deep in the hierarchy") the action is.
/// </summary>
public List<(int indent, EventAction action)> GetAllActions()
{
var list = new List<(int indent, EventAction action)>();
foreach (EventAction eventAction in Actions)
{
list.AddRange(FindActionsRecursive(eventAction));
}
return list;
static List<(int indent, EventAction action)> FindActionsRecursive(EventAction eventAction, int indent = 1)
{
var eventActions = new List<(int indent, EventAction action)> { (indent, eventAction) };
indent++;
foreach (var action in eventAction.GetSubActions())
{
eventActions.AddRange(FindActionsRecursive(action, indent));
}
return eventActions;
}
}
public void AddTarget(Identifier tag, Entity target)
{
if (target == null)
{
throw new System.ArgumentException("Target was null");
throw new ArgumentException($"Target was null (tag: {tag})");
}
if (target.Removed)
{
throw new System.ArgumentException("Target has been removed");
throw new ArgumentException($"Target has been removed (tag: {tag})");
}
if (!Targets.ContainsKey(tag))
if (Targets.ContainsKey(tag))
{
Targets.Add(tag, new List<Entity>());
}
Targets[tag].Add(target);
if (cachedTargets.ContainsKey(tag))
{
cachedTargets[tag].Add(target);
if (!Targets[tag].Contains(target))
{
Targets[tag].Add(target);
}
}
else
{
cachedTargets.Add(tag, new List<Entity> { target });
Targets.Add(tag, new List<Entity>() { target });
}
if (cachedTargets.ContainsKey(tag))
{
if (!cachedTargets[tag].Contains(target))
{
cachedTargets[tag].Add(target);
}
}
else
{
cachedTargets.Add(tag, Targets[tag].ToList());
}
if (!initialAmounts.ContainsKey(tag))
{
initialAmounts.Add(tag, cachedTargets[tag].Count);
}
}
@@ -88,6 +201,15 @@ namespace Barotrauma
}
}
public int GetInitialTargetCount(Identifier tag)
{
if (initialAmounts.TryGetValue(tag, out int count))
{
return count;
}
return 0;
}
public IEnumerable<Entity> GetTargets(Identifier tag)
{
if (cachedTargets.ContainsKey(tag))
@@ -136,10 +258,25 @@ namespace Barotrauma
}
}
cachedTargets.Add(tag, targetsToReturn);
cachedTargets.Add(tag, targetsToReturn);
if (!initialAmounts.ContainsKey(tag))
{
initialAmounts.Add(tag, targetsToReturn.Count);
}
return targetsToReturn;
}
public void InheritTags(Entity originalEntity, Entity newEntity)
{
foreach (var kvp in Targets)
{
if (kvp.Value.Contains(originalEntity))
{
kvp.Value.Add(newEntity);
}
}
}
public void RemoveTag(Identifier tag)
{
if (tag.IsEmpty) { return; }
@@ -152,8 +289,14 @@ namespace Barotrauma
{
int botCount = 0;
int playerCount = 0;
bool forceRefreshTargets = false;
foreach (Character c in Character.CharacterList)
{
if (c.Removed)
{
forceRefreshTargets = true;
continue;
}
if (c.IsPlayer)
{
playerCount++;
@@ -163,7 +306,7 @@ namespace Barotrauma
botCount++;
}
}
if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled)
if (forceRefreshTargets || Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled)
{
cachedTargets.Clear();
prevEntityCount = Entity.EntityCount;
@@ -204,6 +347,10 @@ namespace Barotrauma
break;
}
}
if (CurrentActionIndex == -1)
{
DebugConsole.AddWarning($"Could not find the GoTo label \"{goTo}\" in the event \"{Prefab.Identifier}\". Ending the event.");
}
}
if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0)