Build 1.1.4.0

This commit is contained in:
Markus Isberg
2023-03-31 18:40:44 +03:00
parent efba17e0ff
commit 9470edead3
483 changed files with 17487 additions and 8548 deletions
@@ -110,7 +110,7 @@ namespace Barotrauma
state = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return;
if (!Submarine.MainSub.AtEitherExit) { return; }
Finish();
state = 2;
@@ -9,6 +9,8 @@ namespace Barotrauma
public event Action Finished;
protected bool isFinished;
public int RandomSeed;
protected readonly EventPrefab prefab;
public EventPrefab Prefab => prefab;
@@ -1,3 +1,4 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -15,20 +16,13 @@ namespace Barotrauma
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.");
}
foreach (var attribute in element.Attributes())
{
if (PropertyConditional.IsValid(attribute) && !IsTargetTagAttribute(attribute))
{
Conditional = new PropertyConditional(attribute);
break;
}
}
Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault();
if (Conditional == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.");
}
static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag";
static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag";
}
private string GetEventName()
@@ -21,7 +21,7 @@ namespace Barotrauma
protected object? value2;
protected object? value1;
protected PropertyConditional.OperatorType Operator { get; set; }
protected PropertyConditional.ComparisonOperatorType Operator { get; set; }
public CheckDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
@@ -56,23 +56,13 @@ namespace Barotrauma
{
if (GameMain.GameSession?.GameMode is not CampaignMode campaignMode) { return false; }
string[] splitString = Condition.Split(' ');
string value;
if (splitString.Length > 0)
(Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition);
if (Operator == PropertyConditional.ComparisonOperatorType.None)
{
//the first part of the string is the operator, skip it
value = string.Join(" ", splitString.Skip(1));
}
else
{
DebugConsole.ThrowError($"{Condition} is too short, it should start with an operator followed by a boolean or a floating point value.");
DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value.");
return false;
}
string op = splitString[0];
Operator = PropertyConditional.GetOperatorType(op);
if (Operator == PropertyConditional.OperatorType.None) { return false; }
if (CheckAgainstMetadata)
{
object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier);
@@ -82,8 +72,8 @@ namespace Barotrauma
{
return Operator switch
{
PropertyConditional.OperatorType.Equals => metadata1 == metadata2,
PropertyConditional.OperatorType.NotEquals => metadata1 != metadata2,
PropertyConditional.ComparisonOperatorType.Equals => metadata1 == metadata2,
PropertyConditional.ComparisonOperatorType.NotEquals => metadata1 != metadata2,
_ => false
};
}
@@ -139,9 +129,9 @@ namespace Barotrauma
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return val1 == val2;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return val1 != val2;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {val2}).");
@@ -166,17 +156,17 @@ namespace Barotrauma
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return MathUtils.NearlyEqual(val1, val2);
case PropertyConditional.OperatorType.GreaterThan:
case PropertyConditional.ComparisonOperatorType.GreaterThan:
return val1 > val2;
case PropertyConditional.OperatorType.GreaterThanEquals:
case PropertyConditional.ComparisonOperatorType.GreaterThanEquals:
return val1 >= val2;
case PropertyConditional.OperatorType.LessThan:
case PropertyConditional.ComparisonOperatorType.LessThan:
return val1 < val2;
case PropertyConditional.OperatorType.LessThanEquals:
case PropertyConditional.ComparisonOperatorType.LessThanEquals:
return val1 <= val2;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return !MathUtils.NearlyEqual(val1, val2);
}
@@ -195,9 +185,9 @@ namespace Barotrauma
bool equals = string.Equals(val1, val2, StringComparison.OrdinalIgnoreCase);
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return equals;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return !equals;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a string (was {Operator} for {val2}).");
@@ -33,7 +33,7 @@ namespace Barotrauma
public int ItemContainerIndex { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
private readonly Identifier[] itemIdentifierSplit;
private readonly Identifier[] itemTags;
@@ -44,13 +44,7 @@ namespace Barotrauma
var conditionalList = new List<PropertyConditional>();
foreach (ContentXElement subElement in element.GetChildElements("conditional"))
{
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionalList.Add(new PropertyConditional(attribute));
}
}
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
conditionals = conditionalList;
@@ -200,7 +200,7 @@ namespace Barotrauma
}
}
private int[] GetEndingOptions()
public int[] GetEndingOptions()
{
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
if (!ContinueConversation) { endings.Add(-1); }
@@ -1,12 +1,13 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
class MissionAction : EventAction
partial class MissionAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionIdentifier { get; set; }
@@ -14,8 +15,10 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")]
public string LocationType { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier RequiredFaction { get; set; }
public ImmutableArray<Identifier> LocationTypes { get; }
[Serialize(0, IsPropertySaveable.Yes, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")]
public int MinLocationDistance { get; set; }
@@ -28,6 +31,8 @@ namespace Barotrauma
private bool isFinished;
private readonly Random random;
public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty)
@@ -38,6 +43,8 @@ namespace Barotrauma
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.");
}
LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>()).ToImmutableArray();
random = new MTRandom(parentEvent.RandomSeed);
}
public override bool IsFinished(ref string goTo)
@@ -56,14 +63,14 @@ namespace Barotrauma
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
Mission unlockedMission = null;
var unlockLocation = FindUnlockLocation();
var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes);
if (unlockLocation == null && CreateLocationIfNotFound)
{
//find an empty location at least 3 steps away, further on the map
var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet<Location>());
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable());
if (emptyLocation != null)
{
emptyLocation.ChangeType(Barotrauma.LocationType.Prefabs[LocationType]);
emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation;
}
}
@@ -72,11 +79,11 @@ namespace Barotrauma
{
if (!MissionIdentifier.IsEmpty)
{
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
}
else if (!MissionTag.IsEmpty)
{
unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag);
unlockedMission = unlockLocation.UnlockMissionByTag(MissionTag, random);
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
@@ -84,7 +91,9 @@ namespace Barotrauma
}
if (unlockedMission != null)
{
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null)
unlockedMission.OriginLocation = campaign.Map.CurrentLocation;
campaign.Map.Discover(unlockLocation, checkTalents: false);
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null)
{
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\".");
}
@@ -99,66 +108,86 @@ namespace Barotrauma
IconColor = unlockedMission.Prefab.IconColor
};
#else
missionsUnlockedThisRound.Add(unlockedMission);
NotifyMissionUnlock(unlockedMission);
#endif
#endif
}
}
else
{
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
}
}
isFinished = true;
}
private Location FindUnlockLocation()
private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable<Identifier> locationTypes)
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1)
if (LocationTypes.Length == 0 && minDistance <= 1)
{
return campaign.Map.CurrentLocation;
}
return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet<Location>());
var currentLocation = campaign.Map.CurrentLocation;
int distance = 0;
HashSet<Location> checkedLocations = new HashSet<Location>();
HashSet<Location> pendingLocations = new HashSet<Location>() { currentLocation };
do
{
List<Location> currentLocations = pendingLocations.ToList();
pendingLocations.Clear();
foreach (var location in currentLocations)
{
checkedLocations.Add(location);
if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes))
{
return location;
}
else
{
foreach (LocationConnection connection in location.Connections)
{
var otherLocation = connection.OtherLocation(location);
if (checkedLocations.Contains(otherLocation)) { continue; }
pendingLocations.Add(otherLocation);
}
}
}
distance++;
} while (pendingLocations.Any());
return null;
}
private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet<Location> checkedLocations)
private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable<Identifier> locationTypes)
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (currLocation.Type.Identifier == locationType && currDistance >= MinLocationDistance &&
(!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X))
if (!RequiredFaction.IsEmpty)
{
return currLocation;
if (location.Faction?.Prefab.Identifier != RequiredFaction &&
location.SecondaryFaction?.Prefab.Identifier != RequiredFaction)
{
return false;
}
}
checkedLocations.Add(currLocation);
foreach (LocationConnection connection in currLocation.Connections)
if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier())))
{
var otherLocation = connection.OtherLocation(currLocation);
if (checkedLocations.Contains(otherLocation)) { continue; }
var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations);
if (unlockLocation != null) { return unlockLocation; }
return false;
}
return null;
if (distance < minDistance)
{
return false;
}
if (unlockFurtherOnMap && location.MapPosition.X < currLocation.MapPosition.X)
{
return false;
}
return true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})";
}
#if SERVER
private void NotifyMissionUnlock(Mission mission)
{
foreach (Client client in GameMain.Server.ConnectedClients)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION);
outmsg.WriteIdentifier(mission.Prefab.Identifier);
outmsg.WriteString(mission.Name.Value);
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
}
#endif
}
}
@@ -0,0 +1,66 @@
namespace Barotrauma
{
class MissionStateAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionIdentifier { get; set; }
public enum OperationType
{
Set,
Add
}
[Serialize(OperationType.Set, IsPropertySaveable.Yes)]
public OperationType Operation { get; set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int State { get; set; }
private bool isFinished;
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
State = element.GetAttributeInt("value", State);
if (MissionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.");
}
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
foreach (Mission mission in GameMain.GameSession.Missions)
{
if (mission.Prefab.Identifier != MissionIdentifier) { continue; }
switch (Operation)
{
case OperationType.Set:
mission.State = State;
break;
case OperationType.Add:
mission.State += 1;
break;
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})";
}
}
}
@@ -0,0 +1,91 @@
namespace Barotrauma
{
class ModifyLocationAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Faction { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier SecondaryFaction { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public string Name { get; set; }
private bool isFinished;
public ModifyLocationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
var location = campaign.Map.CurrentLocation;
if (location != null)
{
if (!Faction.IsEmpty)
{
var faction = campaign.Factions.Find(f => f.Prefab.Identifier == Faction);
if (faction == null)
{
DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\".");
}
else
{
location.Faction = faction;
}
}
if (!SecondaryFaction.IsEmpty)
{
var secondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == SecondaryFaction);
if (secondaryFaction == null)
{
DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\".");
}
else
{
location.SecondaryFaction = secondaryFaction;
}
}
if (!Type.IsEmpty)
{
var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == Type);
if (locationType == null)
{
DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\".");
}
else if (!location.LocationTypeChangesBlocked)
{
location.ChangeType(campaign, locationType);
}
}
if (!string.IsNullOrEmpty(Name))
{
location.ForceName(TextManager.Get(Name).Fallback(Name).Value);
}
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ModifyLocationAction)}";
}
}
}
@@ -1,8 +1,6 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -12,7 +10,7 @@ namespace Barotrauma
public Identifier NPCTag { get; set; }
[Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)]
public CharacterTeamType TeamTag { get; set; }
public CharacterTeamType TeamID { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AddToCrew { get; set; }
@@ -24,10 +22,13 @@ namespace Barotrauma
public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
//backwards compatibility
TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum<CharacterTeamType>("team", TeamID));
var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast<CharacterTeamType>();
if (!enums.Contains(TeamTag))
if (!enums.Contains(TeamID))
{
DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}.");
DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamID}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}.");
}
}
@@ -41,27 +42,34 @@ namespace Barotrauma
foreach (var npc in affectedNpcs)
{
// characters will still remain on friendlyNPC team for rest of the tick
npc.SetOriginalTeam(TeamTag);
if (AddToCrew && (TeamTag == CharacterTeamType.Team1 || TeamTag == CharacterTeamType.Team2))
npc.SetOriginalTeam(TeamID);
foreach (Item item in npc.Inventory.AllItems)
{
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.TeamID = TeamID;
}
}
if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2))
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.AddCharacter(npc);
ChangeItemTeam(Submarine.MainSub, true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems));
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems));
}
}
else if (RemoveFromCrew && (npc.TeamID == CharacterTeamType.Team1 || npc.TeamID == CharacterTeamType.Team2))
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.RemoveCharacter(npc, removeInfo: true);
var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamTag);
var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID);
ChangeItemTeam(sub, false);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamTag, npc.Inventory.AllItems));
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems));
}
}
@@ -72,11 +80,10 @@ namespace Barotrauma
item.AllowStealing = allowStealing;
if (item.GetComponent<Items.Components.WifiComponent>() is { } wifiComponent)
{
wifiComponent.TeamID = TeamTag;
wifiComponent.TeamID = TeamID;
}
if (item.GetComponent<Items.Components.IdCard>() is { } idCard)
{
idCard.TeamID = TeamTag;
idCard.SubmarineSpecificID = 0;
}
}
@@ -39,13 +39,14 @@ namespace Barotrauma
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
if (!(npc.AIController is HumanAIController humanAiController)) { continue; }
if (npc.AIController is not HumanAIController humanAiController) { continue; }
if (Follow)
{
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f
OverridePriority = 100.0f,
IsFollowOrderObjective = true
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -19,8 +19,6 @@ namespace Barotrauma
private IEnumerable<Character> affectedNpcs;
private AIObjectiveGoTo gotoObjective;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
@@ -33,19 +31,18 @@ namespace Barotrauma
if (Wait)
{
gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
var gotoObjective = new AIObjectiveGoTo(
AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f
OverridePriority = 100.0f,
SourceEventAction = this
};
humanAiController.ObjectiveManager.AddObjective(gotoObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
}
else
{
if (gotoObjective != null)
{
gotoObjective.Abandon = true;
}
AbandonGoToObjectives(humanAiController);
}
}
isFinished = true;
@@ -62,17 +59,25 @@ namespace Barotrauma
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || npc.AIController is not HumanAIController) { continue; }
if (gotoObjective != null)
{
gotoObjective.Abandon = true;
}
if (npc.Removed || npc.AIController is not HumanAIController aiController) { continue; }
AbandonGoToObjectives(aiController);
}
affectedNpcs = null;
}
isFinished = false;
}
private void AbandonGoToObjectives(HumanAIController aiController)
{
foreach (var objective in aiController.ObjectiveManager.Objectives)
{
if (objective is AIObjectiveGoTo gotoObjective && gotoObjective.SourceEventAction?.ParentEvent == ParentEvent)
{
gotoObjective.Abandon = true;
}
}
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCWaitAction)} -> (NPCTag: {NPCTag.ColorizeObject()}, Wait: {Wait.ColorizeObject()})";
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -11,17 +9,20 @@ namespace Barotrauma
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemIdentifier { get; set; }
public string ItemIdentifiers { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int Amount { get; set; }
public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (ItemIdentifier.IsEmpty)
private readonly ImmutableHashSet<Identifier> itemIdentifierSplit;
public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (string.IsNullOrEmpty(ItemIdentifiers))
{
ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty));
ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty));
}
itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet();
}
private bool isFinished = false;
@@ -62,7 +63,7 @@ namespace Barotrauma
var item = inventory.FindItem(it =>
it != null &&
!removedItems.Contains(it) &&
(ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true);
(itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(it.Prefab.Identifier)), recursive: true);
if (item == null) { break; }
Entity.Spawner.AddItemToRemoveQueue(item);
removedItems.Add(item);
@@ -70,7 +71,7 @@ namespace Barotrauma
}
else if (target is Item item)
{
if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier)
if (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(item.Prefab.Identifier))
{
Entity.Spawner.AddItemToRemoveQueue(item);
removedItems.Add(item);
@@ -46,43 +46,29 @@ namespace Barotrauma
switch (TargetType)
{
case ReputationType.Faction:
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier);
if (faction != null)
{
faction.Reputation.AddReputation(Increase);
}
else
{
DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found.");
}
break;
}
case ReputationType.Location:
{
Location location = campaign.Map.CurrentLocation;
if (location != null)
{
location.Reputation.AddReputation(Increase);
IEnumerable<Location> locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location);
foreach (Location connectedLocation in locations)
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier);
if (faction != null)
{
Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null");
if (connectedLocation.Reputation != null)
{
connectedLocation.Reputation.AddReputation(Increase / 4);
}
faction.Reputation.AddReputation(Increase);
}
else
{
DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found.");
}
}
break;
}
break;
}
case ReputationType.Location:
{
campaign.Map.CurrentLocation?.Reputation?.AddReputation(Increase);
break;
}
default:
{
DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified.");
break;
}
{
DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified.");
break;
}
}
}
@@ -50,7 +50,7 @@ namespace Barotrauma
public Identifier SpawnPointTag { get; set; }
[Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)]
public CharacterTeamType Team { get; protected set; }
public CharacterTeamType TeamID { get; protected set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")]
public bool RequireSpawnPointTag { get; set; }
@@ -92,6 +92,14 @@ namespace Barotrauma
public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null;
//backwards compatibility
TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID));
if (element.GetAttribute("submarinetype") != null)
{
DebugConsole.ThrowError(
$"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " +
$"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?");
}
}
public override bool IsFinished(ref string goTo)
@@ -118,7 +126,28 @@ namespace Barotrauma
if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty)
{
HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier);
HumanPrefab humanPrefab = null;
if (Level.Loaded?.StartLocation is Location startLocation)
{
humanPrefab =
TryFindHumanPrefab(startLocation.Faction) ??
TryFindHumanPrefab(startLocation.SecondaryFaction);
}
HumanPrefab TryFindHumanPrefab(Faction faction)
{
if (faction == null) { return null; }
return
NPCSet.Get(NPCSetIdentifier,
NPCIdentifier.Replace("[faction]".ToIdentifier(), faction.Prefab.Identifier),
logError: false) ??
//try to spawn a coalition NPC if a correct one can't be found
NPCSet.Get(NPCSetIdentifier,
NPCIdentifier.Replace("[faction]".ToIdentifier(), "coalition".ToIdentifier()),
logError: false);
}
humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true);
if (humanPrefab != null)
{
if (!AllowDuplicates &&
@@ -130,13 +159,13 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
{
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = Team;
newCharacter.TeamID = TeamID;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
if (LootingIsStealing)
{
foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true))
@@ -151,6 +180,18 @@ namespace Barotrauma
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());
#endif
});
}
}
@@ -165,7 +206,7 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawn: newCharacter =>
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter =>
{
if (!TargetTag.IsEmpty && newCharacter != null)
{
@@ -211,7 +252,7 @@ namespace Barotrauma
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawned: onSpawned);
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
}
}
else
@@ -239,10 +280,10 @@ namespace Barotrauma
spawned = true;
}
public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount)
public static Vector2 OffsetSpawnPos(Vector2 pos, float offset)
{
Hull hull = Hull.FindHull(pos);
pos += Rand.Vector(offsetAmount);
Hull hull = Hull.FindHull(pos);
pos += Rand.Vector(offset);
if (hull != null)
{
float margin = 50.0f;
@@ -289,30 +330,24 @@ namespace Barotrauma
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false)
{
bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost;
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed);
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable);
if (moduleFlags != null && moduleFlags.Any())
{
List<WayPoint> spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList();
var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains));
if (spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints;
potentialSpawnPoints = spawnPoints.ToList();
}
}
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed));
var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
if (requireTaggedSpawnPoint || spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
}
}
if (potentialSpawnPoints.None())
{
if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any())
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -7,7 +6,7 @@ namespace Barotrauma
{
private readonly List<StatusEffect> effects = new List<StatusEffect>();
private int actionIndex;
private readonly int actionIndex;
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
@@ -46,25 +45,40 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag);
var eventTargets = ParentEvent.GetTargets(TargetTag);
foreach (StatusEffect effect in effects)
{
foreach (var target in targets)
foreach (var target in eventTargets)
{
if (target is Item targetItem)
if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects);
}
else
{
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
List<ISerializableEntity> nearbyTargets = new List<ISerializableEntity>();
effect.AddNearbyTargets(target.WorldPosition, nearbyTargets);
foreach (var nearbyTarget in nearbyTargets)
{
ApplyOnTarget(nearbyTarget as Entity, effect);
}
continue;
}
ApplyOnTarget(target, effect);
}
}
#if SERVER
ServerWrite(targets);
ServerWrite(eventTargets);
#endif
isFinished = true;
void ApplyOnTarget(Entity target, StatusEffect effect)
{
if (target is Item targetItem)
{
effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects);
}
else
{
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
}
}
}
public override string ToDebugString()
@@ -21,6 +21,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes)]
public bool IgnoreIncapacitatedCharacters { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowHiddenItems { get; set; }
private bool isFinished = false;
public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -119,12 +122,12 @@ namespace Barotrauma
private void TagItemsByIdentifier(Identifier identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier);
ParentEvent.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 && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag));
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag));
}
private void TagHullsByName(Identifier name)
@@ -137,6 +140,11 @@ namespace Barotrauma
ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
}
private bool IsValidItem(Item it)
{
return (!it.HiddenInGame || AllowHiddenItems) && SubmarineTypeMatches(it.Submarine);
}
private bool SubmarineTypeMatches(Submarine sub)
{
if (SubmarineType == SubType.Any) { return true; }
@@ -1,12 +1,13 @@
using System.Xml.Linq;
namespace Barotrauma
namespace Barotrauma
{
class TriggerEventAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Identifier { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool NextRound { get; set; }
private bool isFinished;
public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -26,17 +27,24 @@ namespace Barotrauma
if (GameMain.GameSession?.EventManager != null)
{
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
if (NextRound)
{
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.");
GameMain.GameSession.EventManager.QueuedEventsForNextRound.Enqueue(Identifier);
}
else
{
var ev = eventPrefab.CreateInstance();
if (ev != null)
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev);
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.");
}
else
{
var ev = eventPrefab.CreateInstance();
if (ev != null)
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev);
}
}
}
}
@@ -1,6 +1,3 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class WaitAction : EventAction
@@ -1,9 +1,11 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -12,6 +14,7 @@ namespace Barotrauma
public enum NetworkEventType
{
CONVERSATION,
CONVERSATION_SELECTED_OPTION,
STATUSEFFECT,
MISSION,
UNLOCKPATH
@@ -72,8 +75,7 @@ namespace Barotrauma
private readonly List<Event> activeEvents = new List<Event>();
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<EventPrefab> nonRepeatableEvents = new HashSet<EventPrefab>();
private readonly HashSet<EventSet> usedUniqueSets = new HashSet<EventSet>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
#if DEBUG && SERVER
@@ -100,7 +102,9 @@ namespace Barotrauma
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
private struct TimeStamp
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
private readonly struct TimeStamp
{
public readonly double Time;
public readonly Event Event;
@@ -122,7 +126,8 @@ namespace Barotrauma
public bool Enabled = true;
private MTRandom rand;
private MTRandom random;
private int randomSeed;
public void StartRound(Level level)
{
@@ -134,7 +139,9 @@ namespace Barotrauma
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
#if SERVER
MissionAction.ResetMissionsUnlockedThisRound();
#endif
pathFinder = new PathFinder(WayPoint.WayPointList, false);
totalPathLength = 0.0f;
if (level != null)
@@ -144,23 +151,22 @@ namespace Barotrauma
}
SelectSettings();
int seed = 0;
if (level != null)
{
seed = ToolBox.StringToInt(level.Seed);
randomSeed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier);
randomSeed ^= ToolBox.IdentifierToInt(previousEvent);
}
}
rand = new MTRandom(seed);
random = new MTRandom(randomSeed);
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
EventSet initialEventSet = SelectRandomEvents(
EventSet.Prefabs.ToList(),
requireCampaignSet: playingCampaign,
random: rand);
random: random);
EventSet additiveSet = null;
if (initialEventSet != null && initialEventSet.Additive)
{
@@ -168,7 +174,7 @@ namespace Barotrauma
initialEventSet = SelectRandomEvents(
EventSet.Prefabs.Where(e => !e.Additive).ToList(),
requireCampaignSet: playingCampaign,
random: rand);
random: random);
}
if (initialEventSet != null)
{
@@ -188,14 +194,7 @@ namespace Barotrauma
//if the outpost is connected to a locked connection, create an event to unlock it
if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
{
var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent);
var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e =>
e.BiomeIdentifier.IsEmpty ||
e.BiomeIdentifier == level.LevelData.Biome.Identifier);
var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ?
ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) :
ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand);
var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
@@ -216,7 +215,7 @@ namespace Barotrauma
{
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{
nonRepeatableEvents.Add(ep);
nonRepeatableEvents.Add(ep.Identifier);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
@@ -226,6 +225,21 @@ namespace Barotrauma
}
}
while (QueuedEventsForNextRound.Count > 0 && QueuedEventsForNextRound.Dequeue() is Identifier id)
{
var eventPrefab = EventSet.GetEventPrefab(id);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}.");
continue;
}
var ev = eventPrefab.CreateInstance();
if (ev != null)
{
QueuedEvents.Enqueue(ev);
}
}
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
@@ -358,7 +372,6 @@ namespace Barotrauma
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
usedUniqueSets.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
@@ -370,20 +383,49 @@ namespace Barotrauma
/// <summary>
/// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history
/// </summary>
public void RegisterEventHistory()
public void RegisterEventHistory(bool registerFinishedOnly = false)
{
if (level?.LevelData == null) { return; }
level.LevelData.EventsExhausted = true;
level.LevelData.EventsExhausted = !registerFinishedOnly;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (registerFinishedOnly)
{
foreach (var finishedEvent in finishedEvents)
{
var key = finishedEvent.ParentSet;
if (key == null) { continue; }
if (level.LevelData.FinishedEvents.ContainsKey(key))
{
level.LevelData.FinishedEvents[key] += 1;
}
else
{
level.LevelData.FinishedEvents.Add(key, 1);
}
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values
.SelectMany(v => v)
.Select(e => e.Prefab.Identifier)
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
if (!registerFinishedOnly)
{
level.LevelData.FinishedEvents.Clear();
}
bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
}
public void SkipEventCooldown()
@@ -393,9 +435,9 @@ namespace Barotrauma
private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness)
{
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; }
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; }
float retVal = baseCommonness;
if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; }
if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; }
return retVal;
}
@@ -436,9 +478,13 @@ namespace Barotrauma
}
}
bool isPrefabSuitable(EventPrefab e)
=> (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
!level.LevelData.NonRepeatableEvents.Contains(e);
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)
{
@@ -447,9 +493,9 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".");
}
}
var suitablePrefabSubsets = eventSet.EventPrefabs.Where(
e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray();
e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray();
for (int i = 0; i < applyCount; i++)
{
@@ -462,14 +508,14 @@ namespace Barotrauma
for (int j = 0; j < eventCount; j++)
{
if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; }
EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand);
EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random);
(IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) = subEventPrefab;
if (eventPrefabs != null && rand.NextDouble() <= probability)
if (eventPrefabs != null && random.NextDouble() <= probability)
{
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.RandomSeed = randomSeed;
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
@@ -483,7 +529,7 @@ namespace Barotrauma
}
if (eventSet.ChildSets.Any())
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand);
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random);
if (newEventSet != null)
{
CreateEvents(newEventSet);
@@ -494,9 +540,9 @@ namespace Barotrauma
{
foreach ((IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) in suitablePrefabSubsets)
{
if (rand.NextDouble() > probability) { continue; }
if (random.NextDouble() > probability) { continue; }
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
if (!selectedEvents.ContainsKey(eventSet))
@@ -601,6 +647,10 @@ namespace Barotrauma
private bool IsValidForLocation(EventSet eventSet, Location location)
{
if (location is null) { return true; }
if (!eventSet.Faction.IsEmpty)
{
if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; }
}
var locationType = location.GetLocationType();
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
@@ -728,53 +778,50 @@ namespace Barotrauma
calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval;
}
if (currentIntensity < eventThreshold)
bool recheck = false;
do
{
bool recheck = false;
do
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
{
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (selectedEvents.ContainsKey(eventSet))
{
var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (selectedEvents.ContainsKey(eventSet))
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
{
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
{
activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
};
}
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
};
}
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
recheck = true;
}
}
} while (recheck);
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
recheck = true;
}
}
} while (recheck);
foreach (Event ev in activeEvents)
{
@@ -782,11 +829,11 @@ namespace Barotrauma
{
ev.Update(deltaTime);
}
else if (!finishedEvents.Contains(ev))
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
{
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
{
if (!level.LevelData.EventHistory.Contains(ev.Prefab)) { level.LevelData.EventHistory.Add(ev.Prefab); }
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
}
finishedEvents.Add(ev);
}
@@ -832,30 +879,43 @@ namespace Barotrauma
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup(CharacterPrefab.HumanSpeciesName)) { continue; }
if (character.IsIncapacitated || !character.Enabled || character.IsPet) { continue; }
if (!(character.AIController is EnemyAIController enemyAI)) { continue; }
if (character.AIController is EnemyAIController enemyAI)
{
if (!enemyAI.AIParams.StayInAbyss)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterStrength += enemyAI.CombatStrength;
}
if (!enemyAI.AIParams.StayInAbyss)
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterStrength += enemyAI.CombatStrength;
}
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
if (character.Submarine != null &&
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
{
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
//so let's just go with a fixed value.
//5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max
enemyDanger += 0.2f;
}
}
}
// Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub.
// On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4.
// And if they get inside, we add 0.1 per crawler on that.
@@ -1108,5 +1168,20 @@ namespace Barotrauma
return false;
}
public void Load(XElement element)
{
foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty<Identifier>()))
{
QueuedEventsForNextRound.Enqueue(id);
}
}
public XElement Save()
{
return new XElement("eventmanager",
new XAttribute(nameof(QueuedEventsForNextRound),
string.Join(',', QueuedEventsForNextRound)));
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.Reflection.Emit;
namespace Barotrauma
{
@@ -14,12 +15,12 @@ namespace Barotrauma
public readonly bool TriggerEventCooldown;
public readonly float Commonness;
public readonly Identifier BiomeIdentifier;
public readonly Identifier Faction;
public readonly float SpawnDistance;
public readonly bool UnlockPathEvent;
public readonly string UnlockPathTooltip;
public readonly int UnlockPathReputation;
public readonly string UnlockPathFaction;
public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default)
: base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier))
@@ -40,6 +41,7 @@ namespace Barotrauma
}
BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty);
Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent));
@@ -47,7 +49,6 @@ namespace Barotrauma
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
UnlockPathFaction = element.GetAttributeString("unlockpathfaction", "");
SpawnDistance = element.GetAttributeFloat("spawndistance", 0);
}
@@ -80,5 +81,17 @@ namespace Barotrauma
{
return $"EventPrefab ({Identifier})";
}
public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction)
{
var unlockPathEvents = Prefabs.OrderBy(p => p.Identifier).Where(e => e.UnlockPathEvent);
if (faction != null && unlockPathEvents.Any(e => e.Faction == faction.Prefab.Identifier))
{
unlockPathEvents = unlockPathEvents.Where(e => e.Faction == faction.Prefab.Identifier);
}
return
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ??
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty);
}
}
}
@@ -1,10 +1,9 @@
using System;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -89,7 +88,9 @@ namespace Barotrauma
public readonly LevelData.LevelType LevelType;
public readonly ImmutableArray<Identifier> LocationTypeIdentifiers;
public readonly Identifier Faction;
public readonly bool ChooseRandom;
private readonly int eventCount = 1;
@@ -110,6 +111,8 @@ namespace Barotrauma
public readonly bool IgnoreCoolDown;
public readonly bool IgnoreIntensity;
public readonly bool PerRuin, PerCave, PerWreck;
public readonly bool DisableInHuntingGrounds;
@@ -143,11 +146,12 @@ namespace Barotrauma
public readonly struct SubEventPrefab
{
public SubEventPrefab(Either<Identifier[], EventPrefab> prefabOrIdentifiers, float? commonness, float? probability)
public SubEventPrefab(Either<Identifier[], EventPrefab> prefabOrIdentifiers, float? commonness, float? probability, Identifier factionId)
{
PrefabOrIdentifier = prefabOrIdentifiers;
SelfCommonness = commonness;
SelfProbability = probability;
Faction = factionId;
}
public readonly Either<Identifier[], EventPrefab> PrefabOrIdentifier;
@@ -178,6 +182,8 @@ namespace Barotrauma
public readonly float? SelfProbability;
public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f;
public readonly Identifier Faction;
public void Deconstruct(out IEnumerable<EventPrefab> eventPrefabs, out float commonness, out float probability)
{
eventPrefabs = EventPrefabs;
@@ -260,6 +266,8 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.");
}
Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null);
if (locationTypeStr != null)
{
@@ -282,6 +290,7 @@ namespace Barotrauma
PerWreck = element.GetAttributeBool("perwreck", false);
DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck));
IgnoreIntensity = element.GetAttributeBool("ignoreintensity", parentSet?.IgnoreIntensity ?? false);
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false));
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
@@ -332,15 +341,17 @@ namespace Barotrauma
Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty<Identifier>());
float commonness = subElement.GetAttributeFloat("commonness", -1f);
float probability = subElement.GetAttributeFloat("probability", -1f);
Identifier factionId = subElement.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
eventPrefabs.Add(new SubEventPrefab(
identifiers,
commonness >= 0f ? commonness : (float?)null,
probability >= 0f ? probability : (float?)null));
probability >= 0f ? probability : (float?)null,
factionId));
}
else
{
var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier());
eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability));
eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability, prefab.Faction));
}
break;
}
@@ -365,14 +376,36 @@ namespace Barotrauma
public float GetCommonness(Level level)
{
Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty;
return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness;
if (level.GenerationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(level.GenerationParams.Identifier, out float generationParamsCommonness))
{
return generationParamsCommonness;
}
else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(level.StartOutpost.Info.OutpostGenerationParams.Identifier, out float startOutpostParamsCommonness))
{
return startOutpostParamsCommonness;
}
else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(level.EndOutpost.Info.OutpostGenerationParams.Identifier, out float endOutpostParamsCommonness))
{
return endOutpostParamsCommonness;
}
return DefaultCommonness;
}
public int GetEventCount(Level level)
{
if (level?.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count)) { return eventCount; }
return count;
int finishedEventCount = 0;
if (level is not null)
{
level.LevelData.FinishedEvents.TryGetValue(this, out finishedEventCount);
}
if (level.StartLocation == null || !overrideEventCount.TryGetValue(level.StartLocation.Type.Identifier, out int count))
{
return eventCount - finishedEventCount;
}
return count - finishedEventCount;
}
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100, Func<MonsterEvent, bool> filter = null, bool fullLog = false)
@@ -1,5 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -28,6 +27,8 @@ namespace Barotrauma
private const float EndDelay = 5.0f;
private float endTimer;
private bool allowOrderingRescuees;
public override bool AllowRespawn => false;
public override bool AllowUndocking
@@ -39,17 +40,17 @@ namespace Barotrauma
}
}
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
if (State == 0)
{
return Enumerable.Empty<Vector2>();
return Targets.Select(t => (Prefab.SonarLabel, t.WorldPosition));
}
else
{
return Targets.Select(t => t.WorldPosition);
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
}
}
@@ -83,6 +84,8 @@ namespace Barotrauma
{
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true);
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag);
@@ -144,10 +147,7 @@ namespace Barotrauma
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPoint == null)
{
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
}
spawnPoint ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
@@ -186,7 +186,12 @@ namespace Barotrauma
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
@@ -198,7 +203,7 @@ namespace Barotrauma
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found");
continue;
}
for (int i = 0; i < count; i++)
@@ -214,19 +219,25 @@ namespace Barotrauma
{
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human,
SpawnAction.SpawnLocationType.Outpost, spawnPointType,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
}
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos);
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
foreach (Identifier tag in humanPrefab.GetTags())
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
}
}
if (spawnPos is WayPoint wp)
{
@@ -237,9 +248,19 @@ namespace Barotrauma
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
if (allowOrderingRescuees)
{
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
}
#endif
}
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi)
{
var order = OrderPrefab.Prefabs["fightintruders"]
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
}
if (element.GetAttributeBool("requirekill", false))
{
@@ -252,10 +273,7 @@ namespace Barotrauma
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
}
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
characters.Add(spawnedCharacter);
if (element.GetAttributeBool("requirekill", false))
@@ -18,17 +18,19 @@ namespace Barotrauma
private Ruin TargetRuin { get; set; }
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State == 0)
{
return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition);
return allTargets
.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c)))
.Select(t => (Prefab.SonarLabel, t.WorldPosition));
}
else
{
return Enumerable.Empty<Vector2>();
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
}
}
@@ -164,7 +166,7 @@ namespace Barotrauma
{
bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ?
campaign.GetAvailableTransition() != CampaignMode.TransitionType.None :
Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit);
Submarine.MainSub is { } sub && sub.AtEitherExit;
return State > 0 && exitingLevel;
}
@@ -69,15 +69,7 @@ namespace Barotrauma
}
}
public override LocalizedString SonarLabel
{
get
{
return base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
}
}
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
@@ -85,7 +77,12 @@ namespace Barotrauma
{
yield break;
}
yield return level.BeaconStation.WorldPosition;
else
{
yield return (
Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel,
level.BeaconStation.WorldPosition);
}
}
}
@@ -0,0 +1,295 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
partial class EndMission : Mission
{
enum MissionPhase
{
Initial,
NoItemsDestroyed,
SomeItemsDestroyed,
AllItemsDestroyed,
BossKilled
}
private readonly CharacterPrefab bossPrefab;
private readonly CharacterPrefab minionPrefab;
private readonly Identifier spawnPointTag;
private readonly Identifier destructibleItemTag;
private readonly string endCinematicSound;
private ImmutableArray<Character> minions;
private readonly int minionCount;
private readonly float minionScatter;
private Character boss;
private readonly ItemPrefab projectilePrefab;
private float projectileTimer = 30.0f;
private readonly float startCinematicDistance = 30.0f;
private float endCinematicTimer;
private readonly List<Item> destructibleItems = new List<Item>();
protected readonly float wakeUpCinematicDelay = 5.0f;
protected readonly float bossWakeUpDelay = 7.0f;
protected readonly float cameraWaitDuration = 7.0f;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get { return destructibleItems.Where(it => it.Condition > 0.0f).Select(it => (Prefab.SonarLabel, it.WorldPosition)); }
}
public override int State
{
get { return base.State; }
set
{
if (state != value)
{
base.State = value;
OnStateChangedProjSpecific();
if (Phase == MissionPhase.AllItemsDestroyed)
{
CoroutineManager.Invoke(() =>
{
if (boss != null && !boss.Removed)
{
boss.AnimController.ColliderIndex = 1;
}
}, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2);
}
}
}
}
private MissionPhase Phase
{
get
{
//state 0: nothing happens yet, play a cinematic and skip to the next state when close enough to the boss
//state 1: start cinematic played
//state 2: first destructibleItems destroyed
//state 3: 2nd destructibleItems destroyed
//state 4: all destructibleItems destroyed
//state 5: boss killed
if (state == 0) { return MissionPhase.Initial; }
if (state == 1) { return MissionPhase.NoItemsDestroyed; }
if (state < destructibleItems.Count + 1) { return MissionPhase.SomeItemsDestroyed; }
if (state < destructibleItems.Count + 2) { return MissionPhase.AllItemsDestroyed; }
return MissionPhase.BossKilled;
}
}
public EndMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("bossfile", Identifier.Empty);
if (!speciesName.IsEmpty)
{
bossPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (bossPrefab == null)
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
else
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set.");
}
Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty);
if (!minionName.IsEmpty)
{
minionPrefab = CharacterPrefab.FindBySpeciesName(minionName);
if (minionPrefab == null)
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
minionCount = Math.Min(prefab.ConfigElement.GetAttributeInt(nameof(minionCount), 0), 255);
minionScatter = Math.Min(prefab.ConfigElement.GetAttributeFloat(nameof(minionScatter), 0), 10000);
Identifier projectileId = prefab.ConfigElement.GetAttributeIdentifier("projectile", Identifier.Empty);
if (!projectileId.IsEmpty)
{
projectilePrefab = MapEntityPrefab.FindByIdentifier(projectileId) as ItemPrefab;
if (projectilePrefab == null)
{
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\".");
}
}
spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty);
destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), Identifier.Empty);
endCinematicSound = prefab.ConfigElement.GetAttributeString(nameof(endCinematicSound), string.Empty);
startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0);
}
protected override void StartMissionSpecific(Level level)
{
var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag));
if (spawnPoint == 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);
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));
angle += angleStep;
}
SwarmBehavior.CreateSwarm(minionList.Cast<AICharacter>());
minions = minionList.ToImmutableArray();
}
if (destructibleItemTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set.");
return;
}
destructibleItems.Clear();
destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag)));
if (destructibleItems.None())
{
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".");
return;
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
UpdateProjSpecific();
if (state == 0)
{
if (startCinematicDistance <= 0.0f ||
boss == null || Submarine.MainSub == null ||
Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, boss.WorldPosition) <= startCinematicDistance * startCinematicDistance)
{
State = 1;
}
return;
}
if (!IsClient && State > 0)
{
State = Math.Max(State, destructibleItems.Count(it => it.Condition <= 0.0f) + 1);
}
if (Phase == MissionPhase.AllItemsDestroyed)
{
if (projectilePrefab != null && boss != null && !boss.IsDead && !boss.Removed)
{
projectileTimer -= deltaTime;
if (projectileTimer <= 0.0f)
{
float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition);
float distanceFactor = Math.Min(dist / 10000.0f, 1.0f);
int projectileAmount = Rand.Range(3, 6);
//more concentrated shots the further the sub is
float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)) * Math.Max(1.0f - distanceFactor, 0.2f);
for (int i = 0; i < projectileAmount; i++)
{
int index = i;
Entity.Spawner.AddItemToSpawnQueue(projectilePrefab, boss.WorldPosition, onSpawned: it =>
{
var projectile = it.GetComponent<Projectile>();
float angle = MathUtils.VectorToAngle(Submarine.MainSub.WorldPosition - boss.WorldPosition);
if (projectileAmount > 1)
{
angle += (index / (float)(projectileAmount - 1) - 0.5f) * spread;
}
it.body.SetTransform(it.SimPosition, angle);
it.UpdateTransform();
//faster launch velocity the further the sub is
projectile.Use(launchImpulseModifier: MathHelper.Lerp(0, 5, distanceFactor));
});
}
//the closer the sub is, more likely it is to shoot frequently
float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, distanceFactor);
if (Rand.Range(0.0f, 1.0f) < shortIntervalProbability)
{
projectileTimer = Rand.Range(3.0f, 5.0f);
}
else
{
projectileTimer = Rand.Range(15f, 30f);
}
}
}
else
{
State = Math.Max(destructibleItems.Count + 2, State);
}
}
else if (Phase == MissionPhase.BossKilled)
{
const float EndCinematicDuration = 20.0f;
endCinematicTimer += deltaTime;
#if CLIENT
Screen.Selected.Cam.Shake = MathHelper.Clamp(MathF.Pow(endCinematicTimer, 3), 5.0f, 200.0f);
Screen.Selected.Cam.Rotation =
Math.Max((endCinematicTimer - 5.0f) * 0.05f, 0.0f)
+ (PerlinNoise.GetPerlin(endCinematicTimer * 0.1f, endCinematicTimer * 0.05f) - 0.5f) * 0.5f * (endCinematicTimer / EndCinematicDuration);
if (Rand.Range(0.0f, 100.0f) < endCinematicTimer)
{
Level.Loaded.Renderer.Flash();
}
Level.Loaded.Renderer.ChromaticAberrationStrength = endCinematicTimer * 5;
Level.Loaded.Renderer.CollapseEffectOrigin = boss.WorldPosition;
Level.Loaded.Renderer.CollapseEffectStrength = endCinematicTimer / EndCinematicDuration;
#endif
if (endCinematicTimer > 5 && !IsClient)
{
foreach (Character c in Character.CharacterList)
{
if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null)
{
c.SetAllDamage(200.0f, 0.0f, 0.0f);
}
}
}
if (endCinematicTimer > EndCinematicDuration && !IsClient)
{
//endCinematicTimer = 0;
GameMain.GameSession.Campaign?.LoadNewLevel();
}
}
}
partial void UpdateProjSpecific();
partial void OnStateChangedProjSpecific();
protected override bool DetermineCompleted()
{
return Phase == MissionPhase.BossKilled;
}
}
}
@@ -10,11 +10,12 @@ namespace Barotrauma
{
partial class EscortMission : Mission
{
private readonly XElement characterConfig;
private readonly XElement itemConfig;
private readonly ContentXElement characterConfig;
private readonly ContentXElement itemConfig;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
private readonly int baseEscortedCharacters;
private readonly float scalingEscortedCharacters;
@@ -28,7 +29,8 @@ namespace Barotrauma
private readonly List<Character> terroristCharacters = new List<Character>();
private bool terroristsShouldAct = false;
private float terroristDistanceSquared;
private const string TerroristTeamChangeIdentifier = "terrorist";
private const string TerroristTeamChangeIdentifier = "terrorist";
private readonly string terroristAnnounceDialogTag = string.Empty;
public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
@@ -39,6 +41,7 @@ namespace Barotrauma
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems");
terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty);
CalculateReward();
}
@@ -96,14 +99,27 @@ namespace Barotrauma
}
List<HumanPrefab> humanPrefabsToSpawn = new List<HumanPrefab>();
foreach (XElement element in characterConfig.Elements())
foreach (ContentXElement characterElement in characterConfig.Elements())
{
int count = CalculateScalingEscortedCharacterCount(inMission: true);
var humanPrefab = GetHumanPrefabFromElement(element);
var humanPrefab = GetHumanPrefabFromElement(characterElement);
for (int i = 0; i < count; i++)
{
humanPrefabsToSpawn.Add(humanPrefab);
}
foreach (var element in characterElement.Elements())
{
if (element.NameAsIdentifier() == "statuseffect")
{
var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value);
if (newEffect == null) { continue; }
if (!characterStatusEffects.ContainsKey(humanPrefab))
{
characterStatusEffects[humanPrefab] = new List<StatusEffect> { newEffect };
}
characterStatusEffects[humanPrefab].Add(newEffect);
}
}
}
//if any of the escortees have a job defined, try to use a spawnpoint designated for that job
@@ -128,6 +144,13 @@ namespace Barotrauma
{
humanAI.InitMentalStateManager();
}
if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList))
{
foreach (var statusEffect in statusEffectList)
{
statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
}
@@ -162,7 +185,7 @@ namespace Barotrauma
}
int i = 0;
foreach (XElement element in characterConfig.Elements())
foreach (ContentXElement element in characterConfig.Elements())
{
string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
string colorIdentifier = element.GetAttributeString("color", string.Empty);
@@ -231,7 +254,10 @@ namespace Barotrauma
if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands)
{
character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f));
if (!string.IsNullOrEmpty(terroristAnnounceDialogTag))
{
character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f));
}
XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
if (randomElement != null)
{
@@ -1,4 +1,6 @@
namespace Barotrauma
using System;
namespace Barotrauma
{
partial class GoToMission : Mission
{
@@ -11,7 +13,7 @@
{
if (Level.Loaded?.Type == LevelData.LevelType.Outpost)
{
State = 1;
State = Math.Max(1, State);
}
}
@@ -11,20 +11,7 @@ namespace Barotrauma
{
partial class MineralMission : Mission
{
private struct ResourceCluster
{
public int Amount;
public float Rotation;
public ResourceCluster(int amount, float rotation)
{
Amount = amount;
Rotation = rotation;
}
public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation);
}
private readonly Dictionary<Identifier, ResourceCluster> resourceClusters = new Dictionary<Identifier, ResourceCluster>();
private readonly Dictionary<Identifier, int> resourceAmounts = new Dictionary<Identifier, int>();
private readonly Dictionary<Identifier, List<Item>> spawnedResources = new Dictionary<Identifier, List<Item>>();
private readonly Dictionary<Identifier, Item[]> relevantLevelResources = new Dictionary<Identifier, Item[]>();
private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>();
@@ -50,13 +37,13 @@ namespace Barotrauma
/// </summary>
private readonly float resourceHandoverAmount;
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
return missionClusterPositions
.Where(p => spawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(spawnedResources[p.Item1]))
.Select(p => p.Item2);
.Where(p => spawnedResources.ContainsKey(p.Identifier) && AnyAreUncollected(spawnedResources[p.Identifier]))
.Select(p => (ModifyMessage(Prefab.SonarLabel, color: false), p.Position));
}
}
@@ -64,7 +51,6 @@ namespace Barotrauma
public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage);
public override LocalizedString Description => ModifyMessage(description);
public override LocalizedString Name => ModifyMessage(base.Name, false);
public override LocalizedString SonarLabel => ModifyMessage(base.SonarLabel, false);
public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
@@ -82,13 +68,13 @@ namespace Barotrauma
{
var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty) { continue; }
if (resourceClusters.ContainsKey(identifier))
if (resourceAmounts.ContainsKey(identifier))
{
resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation);
resourceAmounts[identifier]++;
}
else
{
resourceClusters.Add(identifier, (1, 0.0f));
resourceAmounts.Add(identifier, 1);
}
}
}
@@ -129,7 +115,7 @@ namespace Barotrauma
if (IsClient) { return; }
foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters)
foreach ((Identifier identifier, int amount) in resourceAmounts)
{
if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab)
{
@@ -137,10 +123,10 @@ namespace Barotrauma
continue;
}
var spawnedResources = level.GenerateMissionResources(prefab, cluster.Amount, positionType, out float rotation, caves);
if (spawnedResources.Count < cluster.Amount)
var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves);
if (spawnedResources.Count < amount)
{
DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{cluster.Amount} of {prefab.Name}");
DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}");
}
if (spawnedResources.None()) { continue; }
@@ -175,7 +161,7 @@ namespace Barotrauma
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
if (!Submarine.MainSub.AtEitherExit) { return; }
State = 2;
break;
}
@@ -195,7 +181,7 @@ namespace Barotrauma
{
// When mission is completed successfully, half of the resources will be removed from the player (i.e. given to the outpost as a part of the mission)
var handoverResources = new List<Item>();
foreach (Identifier identifier in resourceClusters.Keys)
foreach (Identifier identifier in resourceAmounts.Keys)
{
if (relevantLevelResources.TryGetValue(identifier, out var availableResources))
{
@@ -232,11 +218,11 @@ namespace Barotrauma
private void FindRelevantLevelResources()
{
relevantLevelResources.Clear();
foreach (var identifier in resourceClusters.Keys)
foreach (var identifier in resourceAmounts.Keys)
{
var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier &&
i.Submarine == null && i.ParentInventory == null &&
(!(i.GetComponent<Holdable>() is Holdable h) || (h.Attachable && h.Attached)))
(i.GetComponent<Holdable>() is not Holdable h || (h.Attachable && h.Attached)))
.ToArray();
relevantLevelResources.Add(identifier, items);
}
@@ -244,12 +230,12 @@ namespace Barotrauma
private bool EnoughHaveBeenCollected()
{
foreach (var kvp in resourceClusters)
foreach (var kvp in resourceAmounts)
{
if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources))
{
var collected = availableResources.Count(HasBeenCollected);
var needed = kvp.Value.Amount;
var needed = kvp.Value;
if (collected < needed) { return false; }
}
else
@@ -300,10 +286,10 @@ namespace Barotrauma
protected override LocalizedString ModifyMessage(LocalizedString message, bool color = true)
{
int i = 1;
foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters)
foreach ((Identifier identifier, int amount) in resourceAmounts)
{
Replace($"[resourcename{i}]", ItemPrefab.FindByIdentifier(identifier)?.Name.Value ?? "");
Replace($"[resourcequantity{i}]", cluster.Amount.ToString());
Replace($"[resourcequantity{i}]", amount.ToString());
i++;
}
Replace("[handoverpercentage]", ToolBox.GetFormattedPercentage(resourceHandoverAmount));
@@ -22,7 +22,7 @@ namespace Barotrauma
public virtual int State
{
get { return state; }
protected set
set
{
if (state != value)
{
@@ -30,6 +30,11 @@ namespace Barotrauma
TryTriggerEvents(state);
#if SERVER
GameMain.Server?.UpdateMissionState(this);
#elif CLIENT
if (Prefab.ShowProgressBar)
{
CharacterHUD.ShowMissionProgressBar(this);
}
#endif
ShowMessage(State);
OnMissionStateChanged?.Invoke(this);
@@ -37,6 +42,8 @@ namespace Barotrauma
}
}
public int TimesAttempted { get; set; }
protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
private readonly CheckDataAction completeCheckDataAction;
@@ -44,6 +51,12 @@ namespace Barotrauma
public readonly ImmutableArray<LocalizedString> Headers;
public readonly ImmutableArray<LocalizedString> Messages;
/// <summary>
/// The reward that was actually given from completing the mission, taking any talent bonuses into account
/// (some of which may not be possible to determine in advance)
/// </summary>
private int? finalReward;
public virtual LocalizedString Name => Prefab.Name;
private readonly LocalizedString successMessage;
@@ -113,15 +126,19 @@ namespace Barotrauma
get { return null; }
}
public virtual IEnumerable<Vector2> SonarPositions
public virtual IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get { return Enumerable.Empty<Vector2>(); }
get { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); }
}
public virtual LocalizedString SonarLabel => Prefab.SonarLabel;
public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier;
/// <summary>
/// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission.
/// Defaults to Locations[0]
/// </summary>
public Location OriginLocation;
public readonly Location[] Locations;
public int? Difficulty
@@ -141,7 +158,7 @@ namespace Barotrauma
}
}
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Action<Mission> OnMissionStateChanged;
@@ -157,12 +174,13 @@ namespace Barotrauma
Headers = prefab.Headers;
var messages = prefab.Messages.ToArray();
OriginLocation = locations[0];
Locations = locations;
var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction));
if (endConditionElement != null)
{
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})");
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})");
}
for (int n = 0; n < 2; n++)
@@ -307,7 +325,7 @@ namespace Barotrauma
private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger)
{
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
if (trigger.Delay > 0)
if (trigger.Delay > 0 || trigger.State == 0)
{
if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger))
{
@@ -357,6 +375,8 @@ namespace Barotrauma
GiveReward();
}
TimesAttempted++;
EndMissionSpecific(completed);
}
@@ -364,6 +384,27 @@ namespace Barotrauma
protected virtual void EndMissionSpecific(bool completed) { }
/// <summary>
/// Get the final reward, taking talent bonuses into account if the mission has concluded and the talents modified the reward accordingly.
/// </summary>
public int GetFinalReward(Submarine sub)
{
return finalReward ?? GetReward(sub);
}
/// <summary>
/// Calculates the final reward after talent bonuses have been applied. Note that this triggers talent effects of the type OnGainMissionMoney,
/// and should only be called once when the mission is completed!
/// </summary>
private void CalculateFinalReward(Submarine sub)
{
int reward = GetReward(sub);
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
}
private void GiveReward()
{
@@ -407,39 +448,35 @@ namespace Barotrauma
info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value));
}
// apply money gains afterwards to prevent them from affecting XP gains
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
int totalReward = (int)(reward * missionMoneyGainMultiplier.Value);
GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value);
CalculateFinalReward(Submarine.MainSub);
#if SERVER
totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward);
finalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), finalReward.Value);
#endif
bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isSingleplayerOrServer && totalReward > 0)
if (isSingleplayerOrServer)
{
campaign.Bank.Give(totalReward);
}
foreach (Character character in crewCharacters)
{
character.Info.MissionsCompletedSinceDeath++;
}
foreach (KeyValuePair<Identifier, float> reputationReward in ReputationRewards)
{
if (reputationReward.Key == "location")
if (finalReward > 0)
{
Locations[0].Reputation.AddReputation(reputationReward.Value);
Locations[1].Reputation.AddReputation(reputationReward.Value);
campaign.Bank.Give(finalReward.Value);
}
else
foreach (Character character in crewCharacters)
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key);
if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); }
character.Info.MissionsCompletedSinceDeath++;
}
foreach (KeyValuePair<Identifier, float> reputationReward in ReputationRewards)
{
if (reputationReward.Key == "location")
{
OriginLocation.Reputation?.AddReputation(reputationReward.Value);
}
else
{
Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key);
float prevValue = faction.Reputation.Value;
faction?.Reputation.AddReputation(reputationReward.Value);
}
}
}
@@ -484,18 +521,15 @@ namespace Barotrauma
float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f;
int rewardPercentage = (int)(rewardWeight * 100);
return reward switch
{
Some<int> { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum),
None<int> _ => (0, rewardPercentage, sum),
_ => throw new ArgumentOutOfRangeException()
};
int amount = reward.TryUnwrap(out var a) ? a : 0;
return ((int)(amount * rewardWeight), rewardPercentage, sum);
}
protected void ChangeLocationType(LocationTypeChange change)
{
if (change == null) { throw new ArgumentException(); }
if (GameMain.GameSession.GameMode is CampaignMode && !IsClient)
if (GameMain.GameSession.GameMode is CampaignMode campaign && !IsClient)
{
int srcIndex = -1;
for (int i = 0; i < Locations.Length; i++)
@@ -509,13 +543,15 @@ namespace Barotrauma
if (srcIndex == -1) { return; }
var location = Locations[srcIndex];
if (location.LocationTypeChangesBlocked) { return; }
if (change.RequiredDurationRange.X > 0)
{
location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab);
}
else
{
location.ChangeType(LocationType.Prefabs[change.ChangeToType]);
location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]);
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
}
}
@@ -529,7 +565,6 @@ namespace Barotrauma
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters.");
return null;
}
@@ -538,7 +573,7 @@ namespace Barotrauma
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found");
DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".");
return null;
}
@@ -557,8 +592,7 @@ namespace Barotrauma
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.HumanPrefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false);
humanPrefab.GiveItems(spawnedCharacter, submarine, positionToStayIn as WayPoint, Rand.RandSync.ServerAndClient, createNetworkEvents: false);
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
@@ -25,7 +25,8 @@ namespace Barotrauma
GoTo = 0x400,
ScanAlienRuins = 0x800,
ClearAlienRuins = 0x1000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins
End = 0x2000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End
}
partial class MissionPrefab : PrefabWithUintIdentifier
@@ -45,14 +46,15 @@ namespace Barotrauma
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) },
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
{ MissionType.ClearAlienRuins, typeof(AlienRuinMission) }
{ MissionType.ClearAlienRuins, typeof(AlienRuinMission) },
{ MissionType.End, typeof(EndMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
{ MissionType.Combat, typeof(CombatMission) }
};
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo };
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo, MissionType.End };
private readonly ConstructorInfo constructor;
@@ -62,11 +64,7 @@ namespace Barotrauma
public readonly Identifier TextIdentifier;
private readonly string[] tags;
public IEnumerable<string> Tags
{
get { return tags; }
}
public readonly ImmutableHashSet<Identifier> Tags;
public readonly LocalizedString Name;
public readonly LocalizedString Description;
@@ -93,10 +91,24 @@ namespace Barotrauma
public readonly bool AllowRetry;
public readonly bool ShowInMenus, ShowStartMessage;
public readonly bool IsSideObjective;
public readonly bool AllowOtherMissionsInLevel;
public readonly bool RequireWreck, RequireRuin;
/// <summary>
/// If enabled, locations this mission takes place in cannot change their type
/// </summary>
public readonly bool BlockLocationTypeChanges;
public readonly bool ShowProgressBar;
public readonly bool ShowProgressInNumbers;
public readonly int MaxProgressState;
public readonly LocalizedString ProgressBarLabel;
/// <summary>
/// The mission can only be received when travelling from a location of the first type to a location of the second type
/// </summary>
@@ -144,7 +156,7 @@ namespace Barotrauma
TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier);
tags = element.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
string nameTag = element.GetAttributeString("name", "");
Name = TextManager.Get($"MissionName.{TextIdentifier}");
@@ -167,16 +179,26 @@ namespace Barotrauma
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
ShowInMenus = element.GetAttributeBool("showinmenus", true);
ShowStartMessage = element.GetAttributeBool("showstartmessage", true);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool("requirewreck", false);
RequireRuin = element.GetAttributeBool("requireruin", false);
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
Commonness = element.GetAttributeInt("commonness", 1);
AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true);
if (element.GetAttribute("difficulty") != null)
{
int difficulty = element.GetAttributeInt("difficulty", MinDifficulty);
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false);
ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false);
MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1);
string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), "");
ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel);
string successMessageTag = element.GetAttributeString("successmessage", "");
SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}");
if (!string.IsNullOrEmpty(successMessageTag))
@@ -350,6 +372,7 @@ namespace Barotrauma
{
return
AllowedLocationTypes.Any(lt => lt == "any") ||
AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) ||
AllowedLocationTypes.Any(lt => lt == from.Type.Identifier);
}
@@ -357,11 +380,11 @@ namespace Barotrauma
{
if (fromType == "any" ||
fromType == from.Type.Identifier ||
(fromType == "anyoutpost" && from.HasOutpost()))
(fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned"))
{
if (toType == "any" ||
toType == to.Type.Identifier ||
(toType == "anyoutpost" && to.HasOutpost()))
(toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned"))
{
return true;
}
@@ -16,17 +16,20 @@ namespace Barotrauma
private readonly Level.PositionType spawnPosType;
private Vector2? spawnPos = null;
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
yield break;
}
else
{
return sonarPositions;
foreach (Vector2 sonarPos in sonarPositions)
{
yield return (Prefab.SonarLabel, sonarPos);
}
}
}
}
@@ -31,17 +31,17 @@ namespace Barotrauma
private Vector2 nestPosition;
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
{
Enumerable.Empty<Vector2>();
yield break;
}
else
{
yield return nestPosition;
yield return (Prefab.SonarLabel, nestPosition);
}
}
}
@@ -260,9 +260,25 @@ namespace Barotrauma
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
for (int i = 0; i < amount; i++)
{
Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true);
Vector2 offsetPosition;
int tries = 0;
do
{
offsetPosition = nestPosition + Rand.Vector(100.0f);
tries++;
if (tries > 10)
{
offsetPosition = nestPosition;
break;
}
} while (Level.Loaded.IsPositionInsideWall(offsetPosition));
Character.Create(monster.Item1.Identifier, offsetPosition, ToolBox.RandomSeed(8), createNetworkEvent: true);
}
}
if (Level.Loaded.IsPositionInsideWall(nestPosition))
{
DebugConsole.AddWarning($"Error in nest mission \"{Prefab.Identifier}\": nest position was inside a wall ({nestPosition}).");
}
monsterPrefabs.Clear();
break;
}
@@ -274,7 +290,7 @@ namespace Barotrauma
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
if (!Submarine.MainSub.AtEitherExit) { return; }
State = 2;
break;
}
@@ -36,23 +36,32 @@ namespace Barotrauma
private readonly List<Vector2> patrolPositions = new List<Vector2>();
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
var empty = Enumerable.Empty<Vector2>();
if (outsideOfSonarRange)
if (!outsideOfSonarRange || state > 1)
{
return State switch
{
0 => patrolPositions,
1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty,
_ => empty,
};
yield break;
}
else
else if (state == 0)
{
return empty;
foreach (Vector2 patrolPos in patrolPositions)
{
yield return (Prefab.SonarLabel, patrolPos);
}
}
else if (state == 1)
{
if (lastSighting.HasValue)
{
yield return (Prefab.SonarLabel, lastSighting.Value);
}
else
{
yield break;
}
}
}
}
@@ -85,6 +94,31 @@ namespace Barotrauma
characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes");
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
//make sure all referenced character types are defined
foreach (XElement characterElement in characterConfig.Elements())
{
var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty);
var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId);
if (characterTypeElement == null)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".");
}
}
//make sure all defined character types can be found from human prefabs
foreach (XElement characterTypeElement in characterTypeConfig.Elements())
{
foreach (XElement characterElement in characterTypeElement.Elements())
{
Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty);
Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty);
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".");
}
}
}
// for campaign missions, set level at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
@@ -100,6 +134,7 @@ namespace Barotrauma
//level already set
return;
}
submarineInfo = null;
levelData = level;
missionDifficulty = level?.Difficulty ?? 0;
@@ -117,8 +152,15 @@ namespace Barotrauma
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!");
return;
}
// maybe a little redundant
var contentFile = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<EnemySubmarineFile>()).FirstOrDefault(x => x.Path == submarinePath);
BaseSubFile contentFile =
GetSubFile<EnemySubmarineFile>(submarinePath) ??
GetSubFile<SubmarineFile>(submarinePath);
BaseSubFile GetSubFile<T>(ContentPath path) where T : BaseSubFile
{
return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<T>()).FirstOrDefault(f => f.Path == submarinePath);
}
if (contentFile == null)
{
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!");
@@ -241,9 +283,10 @@ namespace Barotrauma
// it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
// if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand);
var characterId = element.GetAttributeString("typeidentifier", string.Empty);
for (int i = 0; i < amountCreated; i++)
{
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault();
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault();
if (characterType == null)
{
@@ -253,7 +296,10 @@ namespace Barotrauma
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null);
var humanPrefab = GetHumanPrefabFromElement(variantElement);
if (humanPrefab == null) { continue; }
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null);
if (!commanderAssigned)
{
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
@@ -305,8 +351,9 @@ namespace Barotrauma
if (enemySub == null)
{
DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined.");
// TODO: should we set the state to something here?
DebugConsole.ThrowError(submarineInfo == null ?
$"Error in PirateMission: enemy sub was not created (submarineInfo == null)." :
$"Error in PirateMission: enemy sub was not created.");
return;
}
@@ -345,10 +392,11 @@ namespace Barotrauma
protected override void UpdateMissionSpecific(float deltaTime)
{
if (state >= 2) { return; }
if (state >= 2 || enemySub == null) { return; }
float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange;
if (CheckWinState())
{
State = 2;
@@ -411,6 +459,7 @@ namespace Barotrauma
characters.Clear();
characterItems.Clear();
failed = !completed;
submarineInfo = null;
}
}
}
@@ -5,40 +5,182 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class SalvageMission : Mission
{
private readonly ItemPrefab itemPrefab;
private Item item;
private readonly Level.PositionType spawnPositionType;
private readonly string containerTag;
private readonly string existingItemTag;
private readonly bool showMessageWhenPickedUp;
/// <summary>
/// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list.
/// </summary>
private readonly List<List<StatusEffect>> statusEffects = new List<List<StatusEffect>>();
public override IEnumerable<Vector2> SonarPositions
private class Target
{
get
public Item Item;
/// <summary>
/// Note that the integer values matter here: the state of the target can't go back to a smaller value,
/// and a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
/// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub)
/// </summary>
public enum RetrievalState
{
None = 0,
Interact = 1,
PickedUp = 2,
RetrievedToSub = 3
}
public readonly ItemPrefab ItemPrefab;
public readonly Level.PositionType SpawnPositionType;
public readonly string ContainerTag;
public readonly string ExistingItemTag;
public readonly bool RemoveItem;
public readonly LocalizedString SonarLabel;
public readonly bool AllowContinueBeforeRetrieved;
/// <summary>
/// Does the target need to be picked up or brought to the sub for mission to be considered successful.
/// If None, the target has no effect on the completion of the mission.
/// </summary>
public readonly RetrievalState RequiredRetrievalState;
public readonly bool HideLabelAfterRetrieved;
public bool Retrieved
{
if (item == null)
get
{
Enumerable.Empty<Vector2>();
return RequiredRetrievalState switch
{
RetrievalState.None => true,
RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState,
RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub,
_ => throw new NotImplementedException(),
};
}
}
private RetrievalState state;
public RetrievalState State
{
get { return state; }
set
{
if (value == state) { return; }
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(mission);
#endif
}
}
public bool Interacted;
private readonly SalvageMission mission;
/// <summary>
/// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list.
/// </summary>
public readonly List<List<StatusEffect>> StatusEffects = new List<List<StatusEffect>>();
public Target(ContentXElement element, SalvageMission mission)
{
this.mission = mission;
ContainerTag = element.GetAttributeString("containertag", "");
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub);
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false);
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false);
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
if (!string.IsNullOrEmpty(sonarLabelTag))
{
SonarLabel =
TextManager.Get($"MissionSonarLabel.{sonarLabelTag}")
.Fallback(TextManager.Get(sonarLabelTag))
.Fallback(element.GetAttributeString("sonarlabel", ""));
}
ExistingItemTag = element.GetAttributeString("existingitemtag", "");
RemoveItem = element.GetAttributeBool("removeitem", true);
if (element.GetAttribute("itemname") != null)
{
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())
{
DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\"");
}
}
else
{
yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition;
Identifier itemIdentifier = element.GetAttributeIdentifier("itemidentifier", Identifier.Empty);
if (!itemIdentifier.IsEmpty)
{
ItemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
}
if (ItemPrefab == null)
{
string itemTag = element.GetAttributeString("itemtag", "");
ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab;
}
if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty())
{
DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\"");
}
}
SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin);
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
{
var newEffect = StatusEffect.Load(subElement, parentDebugName: mission.Prefab.Name.Value);
if (newEffect == null) { continue; }
StatusEffects.Add(new List<StatusEffect> { newEffect });
break;
}
case "chooserandom":
StatusEffects.Add(new List<StatusEffect>());
foreach (var effectElement in subElement.Elements())
{
var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value);
if (newEffect == null) { continue; }
StatusEffects.Last().Add(newEffect);
}
break;
}
}
}
public void Reset()
{
state = RetrievalState.None;
Item = null;
}
}
private readonly List<Target> targets = new List<Target>();
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
foreach (var target in targets)
{
if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; }
if (target.Item != null)
{
yield return (
target.SonarLabel ?? Prefab.SonarLabel,
target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition);
}
if (!target.AllowContinueBeforeRetrieved && !target.Retrieved) { break; }
}
}
}
@@ -46,225 +188,254 @@ namespace Barotrauma
public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
containerTag = prefab.ConfigElement.GetAttributeString("containertag", "");
if (prefab.ConfigElement.GetAttribute("itemname") != null)
foreach (ContentXElement subElement in prefab.ConfigElement.Elements())
{
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
if (subElement.NameAsIdentifier() == "target")
{
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
targets.Add(new Target(subElement, this));
}
}
else
if (!targets.Any())
{
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null);
if (itemIdentifier != null)
{
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
}
if (itemPrefab == null)
{
string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", "");
itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab;
}
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier);
}
}
existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", "");
showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false);
string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) ||
!Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType))
{
spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin;
}
foreach (var element in prefab.ConfigElement.Elements())
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
{
var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name.Value);
if (newEffect == null) { continue; }
statusEffects.Add(new List<StatusEffect> { newEffect });
break;
}
case "chooserandom":
statusEffects.Add(new List<StatusEffect>());
foreach (var subElement in element.Elements())
{
var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name.Value);
if (newEffect == null) { continue; }
statusEffects.Last().Add(newEffect);
}
break;
}
targets.Add(new Target(prefab.ConfigElement, this));
}
}
protected override void StartMissionSpecific(Level level)
{
#if SERVER
originalInventoryID = Entity.NullEntityID;
spawnInfo.Clear();
#endif
item = null;
if (!IsClient)
foreach (var target in targets)
{
//ruin/cave/wreck items are allowed to spawn close to the sub
float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ?
0.0f : Level.Loaded.Size.X * 0.3f;
Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f);
if (!string.IsNullOrEmpty(existingItemTag))
bool usedExistingItem = false;
UInt16 originalInventoryID = 0;
byte originalItemContainerIndex = 0;
int originalSlotIndex = 0;
var executedEffectIndices = new List<(int listIndex, int effectIndex)>();
target.Reset();
if (!IsClient)
{
var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag));
switch (spawnPositionType)
//ruin/cave/wreck items are allowed to spawn close to the sub
float minDistance = target.SpawnPositionType switch
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
case Level.PositionType.SidePath:
item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
break;
case Level.PositionType.Ruin:
case Level.PositionType.Wreck:
foreach (Item it in suitableItems)
{
if (it.Submarine?.Info == null) { continue; }
if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; }
if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
{
item = it;
#if SERVER
usedExistingItem = true;
#endif
break;
}
}
break;
}
}
Level.PositionType.Ruin or
Level.PositionType.Cave or
Level.PositionType.Wreck or
Level.PositionType.Outpost => 0.0f,
_ => Level.Loaded.Size.X * 0.3f,
};
Vector2 position =
target.SpawnPositionType == Level.PositionType.None ?
Vector2.Zero :
Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f);
if (item == null)
{
item = new Item(itemPrefab, position, null);
item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
}
for (int i = 0; i < statusEffects.Count; i++)
{
List<StatusEffect> effectList = statusEffects[i];
if (effectList.Count == 0) { continue; }
int effectIndex = Rand.Int(effectList.Count);
var selectedEffect = effectList[effectIndex];
item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position);
#if SERVER
executedEffectIndices.Add(new Pair<int, int>(i, effectIndex));
#endif
}
//try to find a container and place the item inside it
if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null)
{
List<ItemContainer> validContainers = new List<ItemContainer>();
foreach (Item it in Item.ItemList)
if (!string.IsNullOrEmpty(target.ExistingItemTag))
{
if (!it.HasTag(containerTag)) { continue; }
if (!it.IsPlayerTeamInteractable) { continue; }
switch (spawnPositionType)
var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag));
if (GameMain.GameSession?.Missions != null)
{
//don't choose an item that was already chosen as the target for another salvage mission
suitableItems = suitableItems.Where(it =>
GameMain.GameSession.Missions.None(m => m != this && m is SalvageMission salvageMission && salvageMission.targets.Any(t => t.Item == it)));
}
switch (target.SpawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
if (it.Submarine != null) { continue; }
case Level.PositionType.SidePath:
target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
#if SERVER
usedExistingItem = target.Item != null;
#endif
break;
case Level.PositionType.Ruin:
if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
case Level.PositionType.Outpost:
foreach (Item it in suitableItems)
{
if (it.Submarine?.Info == null) { continue; }
if (target.SpawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; }
if (target.SpawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
if (target.SpawnPositionType == Level.PositionType.Outpost && it.Submarine.Info.Type != SubmarineType.Outpost) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
{
target.Item = it;
#if SERVER
usedExistingItem = true;
#endif
break;
}
}
break;
default:
target.Item = suitableItems.FirstOrDefault();
#if SERVER
usedExistingItem = target.Item != null;
#endif
break;
}
var itemContainer = it.GetComponent<ItemContainer>();
if (itemContainer != null && itemContainer.Inventory.CanBePut(item)) { validContainers.Add(itemContainer); }
}
if (validContainers.Any())
if (target.Item == null)
{
var selectedContainer = validContainers.GetRandomUnsynced();
if (selectedContainer.Combine(item, user: null))
if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag))
{
DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}");
continue;
}
target.Item = new Item(target.ItemPrefab, position, null);
target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation);
target.Item.body.FarseerBody.BodyType = BodyType.Kinematic;
}
else if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
{
target.Item.OnInteract += () =>
{
target.Interacted = true;
};
}
for (int i = 0; i < target.StatusEffects.Count; i++)
{
List<StatusEffect> effectList = target.StatusEffects[i];
if (effectList.Count == 0) { continue; }
int effectIndex = Rand.Int(effectList.Count);
var selectedEffect = effectList[effectIndex];
target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position);
#if SERVER
originalInventoryID = selectedContainer.Item.ID;
originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer);
originalSlotIndex = item.ParentInventory?.FindIndex(item) ?? -1;
executedEffectIndices.Add((i, effectIndex));
#endif
} // Placement successful
}
//try to find a container and place the item inside it
if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null)
{
List<ItemContainer> validContainers = new List<ItemContainer>();
foreach (Item it in Item.ItemList)
{
if (!it.HasTag(target.ContainerTag)) { continue; }
if (!it.IsPlayerTeamInteractable) { continue; }
switch (target.SpawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
if (it.Submarine != null) { continue; }
break;
case Level.PositionType.Ruin:
if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine?.Info == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
break;
}
var itemContainer = it.GetComponent<ItemContainer>();
if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); }
}
if (validContainers.Any())
{
var selectedContainer = validContainers.GetRandomUnsynced();
if (selectedContainer.Combine(target.Item, user: null))
{
#if SERVER
originalInventoryID = selectedContainer.Item.ID;
originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer);
originalSlotIndex = target.Item.ParentInventory?.FindIndex(target.Item) ?? -1;
#endif
} // Placement successful
}
}
}
#if SERVER
spawnInfo.Add(
target,
new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices));
#endif
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
if (item == null)
//make body dynamic when picked up
foreach (var target in targets)
{
#if DEBUG
DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)");
#endif
return;
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root == null) { continue; }
if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; }
}
if (IsClient)
if (IsClient) { return; }
for (int i = 0; i < targets.Count; i++)
{
if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
return;
}
switch (State)
{
case 0:
if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; }
if (showMessageWhenPickedUp)
{
if (!(item.GetRootInventoryOwner() is Character)) { return; }
}
else
{
Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine;
if (parentSub == null || parentSub.Info.Type != SubmarineType.Player)
var target = targets[i];
if (i > 0 && !targets[i - 1].AllowContinueBeforeRetrieved && !targets[i - 1].Retrieved) { break; }
if (target.Item == null)
{
#if DEBUG
DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)");
#endif
return;
}
switch (target.State)
{
case Target.RetrievalState.None:
if (target.Interacted)
{
return;
TrySetRetrievalState(Target.RetrievalState.Interact);
}
}
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
}
break;
case Target.RetrievalState.PickedUp:
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine;
if (parentSub != null)
{
if (parentSub.Info.Type == SubmarineType.Player || Level.IsLoadedFriendlyOutpost)
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
}
break;
}
void TrySetRetrievalState(Target.RetrievalState retrievalState)
{
if (retrievalState < target.State) { return; }
bool wasRetrieved = false;
target.State = retrievalState;
//increment the mission state if the target became retrieved
if (!wasRetrieved && target.Retrieved) { State = i + 1; }
}
}
if (targets.All(t => t.Retrieved))
{
State = targets.Count + 1;
}
}
protected override bool DetermineCompleted()
{
var root = item?.GetRootContainer() ?? item;
return root?.CurrentHull?.Submarine != null && (root.CurrentHull.Submarine.AtEndExit || root.CurrentHull.Submarine.AtStartExit) && !item.Removed;
return targets.All(t => t.State >= t.RequiredRetrievalState);
}
protected override void EndMissionSpecific(bool completed)
{
item?.Remove();
item = null;
failed = !completed && state > 0;
//consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level
failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp);
foreach (var target in targets)
{
if (target.RemoveItem)
{
target.Item?.Remove();
target.Reset();
}
}
}
}
}
@@ -32,25 +32,20 @@ namespace Barotrauma
}
}
public override IEnumerable<Vector2> SonarPositions
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0)
if (State > 0 || scanTargets.None())
{
return Enumerable.Empty<Vector2>();
}
else if (scanTargets.Any())
{
return scanTargets
.Where(kvp => !kvp.Value)
.Select(kvp => kvp.Key.WorldPosition);
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
else
{
return Enumerable.Empty<Vector2>();
}
return scanTargets
.Where(kvp => !kvp.Value)
.Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition));
}
}
}
@@ -244,7 +244,12 @@ namespace Barotrauma
float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
if (sub.Info.Type != SubmarineType.Player &&
sub.Info.Type != SubmarineType.EnemySubmarine &&
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
{
continue;
}
float minDistToSub = GetMinDistanceToSub(sub);
if (dist < minDistToSub * minDistToSub) { continue; }
@@ -116,7 +116,7 @@ namespace Barotrauma
{
foreach (Entity entity in Entity.GetEntities())
{
if (targetPredicates[tag].Any(p => p(entity)))
if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity))
{
targetsToReturn.Add(entity);
}
@@ -131,7 +131,7 @@ namespace Barotrauma
{
foreach (Character npc in outpostNPCs)
{
if (npc.Removed) { continue; }
if (npc.Removed || targetsToReturn.Contains(npc)) { continue; }
targetsToReturn.Add(npc);
}
}