Faction Test 100.4.0.0

This commit is contained in:
Markus Isberg
2022-11-14 18:28:28 +02:00
parent 87426b68b2
commit c772b61fc1
412 changed files with 16984 additions and 5530 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;
@@ -25,11 +25,14 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool RequireEquipped { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool Recursive { get; set; }
[Serialize(-1, IsPropertySaveable.Yes)]
public int ItemContainerIndex { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
private readonly Identifier[] itemIdentifierSplit;
private readonly Identifier[] itemTags;
@@ -97,7 +100,7 @@ namespace Barotrauma
{
if (inventory == null) { return false; }
int count = 0;
foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier)))
foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier), recursive: Recursive))
{
if (!ConditionalsMatch(item, character)) { continue; }
count++;
@@ -0,0 +1,58 @@
using System;
using System.Linq;
namespace Barotrauma;
class CheckMissionAction : BinaryOptionAction
{
public enum MissionType
{
Current,
Selected,
Available
}
[Serialize(MissionType.Current, IsPropertySaveable.Yes)]
public MissionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier MissionTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int MissionCount { get; set; }
public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
MissionCount = Math.Max(MissionCount, 0);
}
protected override bool? DetermineSuccess()
{
var missions = Type switch
{
MissionType.Current => GameMain.GameSession?.Missions,
MissionType.Selected => GameMain.GameSession?.Campaign?.Missions,
MissionType.Available => GameMain.GameSession?.Map?.CurrentLocation?.AvailableMissions,
_ => null
};
if (missions is not null)
{
if (!MissionIdentifier.IsEmpty)
{
return missions.Any(m => m.Prefab.Identifier == MissionIdentifier);
}
else if (!MissionTag.IsEmpty)
{
return missions.Count(m => m.Prefab.Tags.Contains(MissionTag.Value)) >= MissionCount;
}
else
{
return missions.Count() >= MissionCount;
}
}
return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionCount == 0;
}
}
@@ -0,0 +1,15 @@
namespace Barotrauma;
partial class CheckObjectiveAction : BinaryOptionAction
{
public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
bool success = false;
DetermineSuccessProjSpecific(ref success);
return success;
}
partial void DetermineSuccessProjSpecific(ref bool success);
}
@@ -1,7 +1,15 @@
using Barotrauma.Extensions;
namespace Barotrauma
{
class CheckOrderAction : BinaryOptionAction
{
public enum OrderPriority
{
Top,
Any
}
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
@@ -14,35 +22,58 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier OrderTargetTag { get; set; }
[Serialize(OrderPriority.Top, IsPropertySaveable.Yes)]
public OrderPriority Priority { get; set; }
public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
Character targetCharacter = null;
if (!TargetTag.IsEmpty)
var targetCharacters = ParentEvent.GetTargets(TargetTag);
if (targetCharacters.None())
{
foreach (var t in ParentEvent.GetTargets(TargetTag))
{
if (t is Character c)
{
targetCharacter = c;
break;
}
}
}
if (targetCharacter == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority();
if (currentOrderInfo?.Identifier == OrderIdentifier)
foreach (var t in targetCharacters)
{
if (!OrderTargetTag.IsEmpty)
if (t is not Character c)
{
if (currentOrderInfo.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)) { return false; }
continue;
}
if (Priority == OrderPriority.Top)
{
if (c.GetCurrentOrderWithTopPriority() is Order topPrioOrder && IsMatch(topPrioOrder))
{
return true;
}
}
else if (Priority == OrderPriority.Any)
{
foreach (var order in c.CurrentOrders)
{
if (IsMatch(order))
{
return true;
}
}
}
bool IsMatch(Order order)
{
if (order?.Identifier == OrderIdentifier)
{
if (!OrderTargetTag.IsEmpty && (order.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)))
{
return false;
}
if (OrderOption.IsEmpty || order?.Option == OrderOption)
{
return true;
}
}
return false;
}
return OrderOption.IsEmpty || currentOrderInfo?.Option == OrderOption;
}
return false;
}
@@ -0,0 +1,87 @@
using System;
using System.Linq;
namespace Barotrauma;
class CheckPurchasedItemsAction : BinaryOptionAction
{
public enum TransactionType
{
Purchased,
Sold
}
[Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)]
public TransactionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemTag { get; set; }
[Serialize(1, IsPropertySaveable.Yes)]
public int MinCount { get; set; }
public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
MinCount = Math.Max(MinCount, 1);
}
protected override bool? DetermineSuccess()
{
if (ItemIdentifier.IsEmpty && ItemTag.IsEmpty)
{
return false;
}
if (GameMain.GameSession?.Campaign?.CargoManager is not CargoManager cargoManager)
{
return false;
}
if (Type == TransactionType.Purchased)
{
int totalPurchased = 0;
foreach ((Identifier id, var items) in cargoManager.PurchasedItems)
{
if (!ItemIdentifier.IsEmpty)
{
totalPurchased += items.Find(i => i.ItemPrefabIdentifier == ItemIdentifier)?.Quantity ?? 0;
}
else if (!ItemTag.IsEmpty)
{
foreach (var item in items)
{
if (item.ItemPrefab.Tags.Contains(ItemTag))
{
totalPurchased += item.Quantity;
}
}
}
if (totalPurchased >= MinCount)
{
return true;
}
}
}
else
{
int totalSold = 0;
foreach ((Identifier id, var items) in cargoManager.SoldItems)
{
if (!ItemIdentifier.IsEmpty)
{
totalSold += items.Count(i => i.ItemPrefab.Identifier == ItemIdentifier);
}
else if (!ItemTag.IsEmpty)
{
totalSold += items.Count(i => i.ItemPrefab.Tags.Contains(ItemTag));
}
if (totalSold >= MinCount)
{
return true;
}
}
}
return false;
}
}
@@ -59,7 +59,10 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool ContinueConversation { get; set; }
public Character speaker
[Serialize(false, IsPropertySaveable.Yes)]
public bool IgnoreInterruptDistance { get; set; }
public Character Speaker
{
get;
private set;
@@ -124,7 +127,7 @@ namespace Barotrauma
#else
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.InGame && c.Character != null) { ServerWrite(speaker, c, interrupt); }
if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); }
}
#endif
ResetSpeaker();
@@ -160,7 +163,7 @@ namespace Barotrauma
selectedOption = -1;
interrupt = false;
dialogOpened = false;
speaker = null;
Speaker = null;
}
public override bool SetGoToTarget(string goTo)
@@ -181,15 +184,14 @@ namespace Barotrauma
private void ResetSpeaker()
{
if (speaker == null) { return; }
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.ActiveConversation = null;
speaker.SetCustomInteract(null, null);
if (Speaker == null) { return; }
Speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
Speaker.ActiveConversation = null;
Speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData());
#endif
var humanAI = speaker.AIController as HumanAIController;
if (humanAI != null && !speaker.IsDead && !speaker.Removed)
if (Speaker.AIController is HumanAIController humanAI && !Speaker.IsDead && !Speaker.Removed)
{
humanAI.ClearForcedOrder();
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
@@ -207,7 +209,6 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
lastActiveTime = Timing.TotalTime;
if (interrupt)
{
Interrupted?.Update(deltaTime);
@@ -216,6 +217,7 @@ namespace Barotrauma
{
if (dialogOpened)
{
lastActiveTime = Timing.TotalTime;
#if CLIENT
if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction"))
{
@@ -226,7 +228,7 @@ namespace Barotrauma
Reset();
}
#endif
if (ShouldInterrupt())
if (ShouldInterrupt(requireTarget: true))
{
ResetSpeaker();
interrupt = true;
@@ -236,34 +238,34 @@ namespace Barotrauma
if (!SpeakerTag.IsEmpty)
{
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (speaker == null || speaker.Removed)
if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (Speaker == null || Speaker.Removed)
{
return;
}
//some conversation already assigned to the speaker, wait for it to be removed
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
if (Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
{
return;
}
else if (!WaitForInteraction)
{
TryStartConversation(speaker);
TryStartConversation(Speaker);
}
else if (speaker.ActiveConversation != this)
else if (Speaker.ActiveConversation != this)
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
speaker.ActiveConversation = this;
Speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
Speaker.ActiveConversation = this;
#if CLIENT
speaker.SetCustomInteract(
Speaker.SetCustomInteract(
TryStartConversation,
TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
#else
speaker.SetCustomInteract(
Speaker.SetCustomInteract(
TryStartConversation,
TextManager.Get("CampaignInteraction.Talk"));
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData());
#endif
}
return;
@@ -275,7 +277,9 @@ namespace Barotrauma
}
else
{
if (ShouldInterrupt())
//after the conversation has been finished and the target character assigned,
//we no longer care if we still have a target
if (ShouldInterrupt(requireTarget: false))
{
ResetSpeaker();
interrupt = true;
@@ -287,35 +291,36 @@ namespace Barotrauma
}
}
private bool ShouldInterrupt()
private bool ShouldInterrupt(bool requireTarget)
{
IEnumerable<Entity> targets = Enumerable.Empty<Entity>();
if (!TargetTag.IsEmpty)
if (!TargetTag.IsEmpty && requireTarget)
{
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e, requireTarget));
if (!targets.Any()) { return true; }
}
if (speaker != null)
if (Speaker != null)
{
if (!TargetTag.IsEmpty)
if (!TargetTag.IsEmpty && requireTarget && !IgnoreInterruptDistance)
{
if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, Speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; }
}
if (speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
if (Speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction())
{
return true;
}
return speaker.Removed || speaker.IsDead || speaker.IsIncapacitated;
return Speaker.Removed || Speaker.IsDead || Speaker.IsIncapacitated;
}
return false;
}
private bool IsValidTarget(Entity e)
private bool IsValidTarget(Entity e, bool requirePlayerControlled = true)
{
bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(e == Character.Controlled || character.IsRemotePlayer);
bool isValid =
e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(character == Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled);
#if SERVER
if (!dialogOpened)
{
@@ -40,7 +40,8 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.");
continue;
}
Actions.Add(Instantiate(scriptedEvent, e));
var action = Instantiate(scriptedEvent, e);
if (action != null) { Actions.Add(action); }
}
}
@@ -149,6 +150,10 @@ namespace Barotrauma
ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(ContentXElement) });
try
{
if (constructor == null)
{
throw new Exception($"Error in scripted event \"{scriptedEvent.Prefab.Identifier}\" - could not find a constructor for the EventAction \"{actionType}\".");
}
return constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction;
}
catch (Exception ex)
@@ -49,6 +49,12 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ObjectiveTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool ObjectiveCanBeCompleted { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ParentObjectiveId { get; set; }
private bool isFinished = false;
public MessageBoxAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
@@ -14,8 +16,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; }
@@ -38,6 +42,7 @@ 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();
}
public override bool IsFinished(ref string goTo)
@@ -56,14 +61,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, Barotrauma.LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation;
}
}
@@ -84,6 +89,7 @@ namespace Barotrauma
}
if (unlockedMission != null)
{
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,46 +105,80 @@ namespace Barotrauma
IconColor = unlockedMission.Prefab.IconColor
};
#else
NotifyMissionUnlock(unlockedMission);
#endif
NotifyMissionUnlock(unlockedMission, unlockLocation);
#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()
@@ -147,7 +187,7 @@ namespace Barotrauma
}
#if SERVER
private void NotifyMissionUnlock(Mission mission)
private static void NotifyMissionUnlock(Mission mission, Location unlockLocation)
{
foreach (Client client in GameMain.Server.ConnectedClients)
{
@@ -155,6 +195,7 @@ namespace Barotrauma
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION);
outmsg.WriteIdentifier(mission.Prefab.Identifier);
outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(unlockLocation) ?? -1);
outmsg.WriteString(mission.Name.Value);
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
@@ -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
{
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,7 +1,6 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -11,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; }
@@ -21,7 +20,17 @@ namespace Barotrauma
private bool isFinished = false;
public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
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(TeamID))
{
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)))}.");
}
}
private List<Character> affectedNpcs = null;
@@ -33,42 +42,48 @@ 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));
}
}
void ChangeItemTeam(Submarine sub, bool allowStealing)
{
foreach (Item item in npc.Inventory.AllItems)
foreach (Item item in npc.Inventory.FindAllItems(recursive: true))
{
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,17 @@ namespace Barotrauma
if (Wait)
{
gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
var gotoObjective = new AIObjectiveGoTo(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 +58,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()})";
@@ -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;
}
}
}
@@ -1,9 +1,8 @@
using System;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -11,6 +10,7 @@ namespace Barotrauma
{
public enum SpawnLocationType
{
Any,
MainSub,
Outpost,
MainPath,
@@ -40,7 +40,7 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")]
public Identifier TargetInventory { get; set; }
[Serialize(SpawnLocationType.MainSub, IsPropertySaveable.Yes)]
[Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes)]
public SpawnLocationType SpawnLocation { get; set; }
[Serialize(SpawnType.Human, IsPropertySaveable.Yes)]
@@ -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,8 @@ namespace Barotrauma
public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null;
//backwards compatibility
TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum<CharacterTeamType>("team", TeamID));
}
public override bool IsFinished(ref string goTo)
@@ -118,7 +120,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,11 +153,11 @@ 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);
if (LootingIsStealing)
@@ -151,6 +174,14 @@ 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);
}
}
});
}
}
@@ -165,7 +196,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)
{
@@ -177,7 +208,7 @@ namespace Barotrauma
}
else if (!ItemIdentifier.IsEmpty)
{
if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab))
if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab)
{
DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)");
}
@@ -211,7 +242,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 +270,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;
@@ -257,21 +288,11 @@ namespace Barotrauma
{
if (!SpawnPointTag.IsEmpty)
{
List<Item> potentialItems = SpawnLocation switch
{
SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub),
SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null),
SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin),
SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon),
_ => throw new NotImplementedException()
};
List<Item> potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine));
var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced();
if (item != null) { return item; }
var target = ParentEvent.GetTargets(SpawnPointTag).GetRandomUnsynced();
var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced();
if (target != null) { return target; }
}
@@ -281,19 +302,26 @@ namespace Barotrauma
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag);
}
private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine)
{
return spawnLocation switch
{
SpawnLocationType.Any => true,
SpawnLocationType.MainSub => submarine == Submarine.MainSub,
SpawnLocationType.MainPath => submarine == null,
SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } },
SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } },
SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } },
SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null,
_ => throw new NotImplementedException(),
};
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false)
{
List<WayPoint> potentialSpawnPoints = spawnLocation switch
{
SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null),
SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null),
SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost),
SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck),
SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin),
SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon),
_ => throw new NotImplementedException()
};
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);
if (moduleFlags != null && moduleFlags.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()
@@ -32,11 +32,13 @@ namespace Barotrauma
("bot", v => TagBots(playerCrewOnly: false)),
("crew", v => TagCrew()),
("humanprefabidentifier", TagHumansByIdentifier),
("jobidentifier", TagHumansByJobIdentifier),
("structureidentifier", TagStructuresByIdentifier),
("structurespecialtag", TagStructuresBySpecialTag),
("itemidentifier", TagItemsByIdentifier),
("itemtag", TagItemsByTag),
("hullname", TagHullsByName)
("hullname", TagHullsByName),
("submarine", TagSubmarinesByType),
}.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary();
}
@@ -93,6 +95,18 @@ namespace Barotrauma
}
}
}
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
{
foreach (Character c in Character.CharacterList)
{
if (c.HasJob(jobIdentifier))
{
ParentEvent.AddTarget(Tag, c);
}
}
}
private void TagStructuresByIdentifier(Identifier identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
@@ -118,6 +132,11 @@ namespace Barotrauma
ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
}
private void TagSubmarinesByType(Identifier type)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
}
private bool SubmarineTypeMatches(Submarine sub)
{
if (SubmarineType == SubType.Any) { return true; }
@@ -8,6 +8,12 @@ namespace Barotrauma
{
class TriggerAction : EventAction
{
public enum TriggerType
{
Inside,
Outside
}
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")]
public Identifier Target1Tag { get; set; }
@@ -23,7 +29,10 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")]
public Identifier ApplyToTarget2 { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Range both entities must be within to activate the trigger.")]
[Serialize(TriggerType.Inside, IsPropertySaveable.Yes, description: "Determines if the targets must be inside or outside of the radius.")]
public TriggerType Type { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Range to activate the trigger.")]
public float Radius { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")]
@@ -38,6 +47,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")]
public bool AllowMultipleTargets { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")]
public bool CheckAllTargets { get; set; }
private float distance;
public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -57,6 +69,8 @@ namespace Barotrauma
public bool isRunning = false;
private readonly List<Either<Character, Item>> npcsOrItems = new List<Either<Character, Item>>();
private readonly List<(Entity e1, Entity e2)> triggerers = new List<(Entity e1, Entity e2)>();
public override void Update(float deltaTime)
{
@@ -66,18 +80,44 @@ namespace Barotrauma
var targets1 = ParentEvent.GetTargets(Target1Tag);
if (!targets1.Any()) { return; }
triggerers.Clear();
foreach (Entity e1 in targets1)
{
if (DisableInCombat && IsInCombat(e1)) { continue; }
if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; }
if (DisableInCombat && IsInCombat(e1))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (!TargetModuleType.IsEmpty)
{
if (IsCloseEnoughToHull(e1, out Hull hull))
if (!CheckAllTargets && CheckDistanceToHull(e1, out Hull hull))
{
Trigger(e1, hull);
return;
}
else if (CheckAllTargets)
{
if (CheckDistanceToHull(e1, out hull))
{
triggerers.Add((e1, hull));
}
else
{
return;
}
}
continue;
}
@@ -85,9 +125,26 @@ namespace Barotrauma
foreach (Entity e2 in targets2)
{
if (e1 == e2) { continue; }
if (DisableInCombat && IsInCombat(e2)) { continue; }
if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; }
if (e1 == e2)
{
continue;
}
if (DisableInCombat && IsInCombat(e2))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated))
{
if (CheckAllTargets)
{
return;
}
continue;
}
if (WaitForInteraction)
{
@@ -173,16 +230,35 @@ namespace Barotrauma
Vector2 pos1 = e1.WorldPosition;
Vector2 pos2 = e2.WorldPosition;
distance = Vector2.Distance(pos1, pos2);
if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius)
if ((Type == TriggerType.Inside) == IsWithinRadius())
{
if (!CheckAllTargets)
{
Trigger(e1, e2);
return;
}
else
{
triggerers.Add((e1, e2));
}
}
else if (CheckAllTargets)
{
Trigger(e1, e2);
return;
}
bool IsWithinRadius() =>
((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius;
}
}
}
}
foreach (var (e1, e2) in triggerers)
{
Trigger(e1, e2);
}
}
private void ResetTargetIcons()
@@ -205,7 +281,7 @@ namespace Barotrauma
}
}
private bool IsCloseEnoughToHull(Entity e, out Hull hull)
private bool CheckDistanceToHull(Entity e, out Hull hull)
{
hull = null;
if (Radius <= 0)
@@ -213,36 +289,35 @@ namespace Barotrauma
if (e is Character character && character.CurrentHull != null && character.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
{
hull = character.CurrentHull;
return true;
return Type == TriggerType.Inside;
}
else if (e is Item item && item.CurrentHull != null && item.CurrentHull.OutpostModuleTags.Contains(TargetModuleType))
{
hull = item.CurrentHull;
return true;
return Type == TriggerType.Inside;
}
return false;
return Type == TriggerType.Outside;
}
else
{
foreach (Hull potentialHull in Hull.HullList)
{
if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; }
Rectangle hullRect = potentialHull.WorldRect;
hullRect.Inflate(Radius, Radius);
if (Submarine.RectContains(hullRect, e.WorldPosition))
{
hull = potentialHull;
return true;
return Type == TriggerType.Inside;
}
}
return false;
return Type == TriggerType.Outside;
}
}
private bool IsInCombat(Entity entity)
private static bool IsInCombat(Entity entity)
{
if (!(entity is Character character)) { return false; }
if (entity is not Character character) { return false; }
foreach (Character c in Character.CharacterList)
{
if (c.IsDead || c.Removed || c.IsIncapacitated || !c.Enabled) { continue; }
@@ -13,6 +13,12 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ObjectiveTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool CanBeCompleted { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ParentObjectiveId { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AutoPlayVideo { get; set; }
@@ -1,6 +1,3 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
class WaitAction : EventAction
@@ -71,8 +71,9 @@ 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<Identifier> finishedEvents = new HashSet<Identifier>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
private readonly HashSet<EventSet> usedUniqueSets = new HashSet<EventSet>();
#if DEBUG && SERVER
@@ -150,17 +151,24 @@ namespace Barotrauma
seed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier);
seed ^= ToolBox.IdentifierToInt(previousEvent);
}
}
rand = new MTRandom(seed);
EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
EventSet initialEventSet = SelectRandomEvents(
EventSet.Prefabs.ToList(),
requireCampaignSet: playingCampaign,
random: rand);
EventSet additiveSet = null;
if (initialEventSet != null && initialEventSet.Additive)
{
additiveSet = initialEventSet;
initialEventSet = SelectRandomEvents(EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
initialEventSet = SelectRandomEvents(
EventSet.Prefabs.Where(e => !e.Additive).ToList(),
requireCampaignSet: playingCampaign,
random: rand);
}
if (initialEventSet != null)
{
@@ -180,14 +188,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();
@@ -201,6 +202,7 @@ namespace Barotrauma
}
AddChildEvents(initialEventSet);
void AddChildEvents(EventSet eventSet)
{
if (eventSet == null) { return; }
@@ -208,7 +210,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)
@@ -351,6 +353,7 @@ namespace Barotrauma
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
usedUniqueSets.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
@@ -364,15 +367,25 @@ namespace Barotrauma
/// </summary>
public void RegisterEventHistory()
{
level.LevelData.EventsExhausted = true;
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
if (level?.LevelData != null)
{
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
level.LevelData.EventsExhausted = true;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab.Identifier).Where(e => !level.LevelData.EventHistory.Contains(e)));
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)));
}
foreach (var usedUniqueSet in usedUniqueSets)
{
if (!level.LevelData.UsedUniqueSets.Contains(usedUniqueSet.Identifier))
{
level.LevelData.UsedUniqueSets.Add(usedUniqueSet.Identifier);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
}
}
@@ -383,9 +396,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;
}
@@ -398,6 +411,11 @@ namespace Barotrauma
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
if (eventSet.Unique && !usedUniqueSets.Contains(eventSet))
{
usedUniqueSets.Add(eventSet);
}
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
@@ -426,9 +444,12 @@ namespace Barotrauma
}
}
bool isPrefabSuitable(EventPrefab e)
=> e.BiomeIdentifier.IsEmpty ||
e.BiomeIdentifier == level.LevelData?.Biome?.Identifier;
bool isPrefabSuitable(EventPrefab e) =>
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.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)
{
@@ -437,9 +458,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++)
{
@@ -496,12 +517,12 @@ namespace Barotrauma
selectedEvents[eventSet].Add(newEvent);
}
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
var location = GetEventLocation();
foreach (EventSet childEventSet in eventSet.ChildSets)
{
if (!IsValidForLevel(childEventSet, level)) { continue; }
if (location != null && !IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet);
if (!IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet);
}
}
}
@@ -536,10 +557,32 @@ namespace Barotrauma
}
}
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
if (location != null)
var location = GetEventLocation();
allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location));
allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly ||
(GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true }));
int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location);
int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location);
if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex))
{
allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location));
allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex);
}
else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex))
{
allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex);
}
else
{
// When there are no forced sets, only allow sets that aren't forced at any specific location
allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0);
}
if (allowedEventSets.Count() == 1)
{
// When there's only a single set available, just select it directly
return allowedEventSets.First();
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
@@ -558,18 +601,31 @@ namespace Barotrauma
return null;
}
private bool IsValidForLevel(EventSet eventSet, Level level)
private static bool IsValidForLevel(EventSet eventSet, Level level)
{
return
level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty &&
level.LevelData.Type == eventSet.LevelType &&
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier) &&
(!eventSet.Unique || !level.LevelData.UsedUniqueSets.Contains(eventSet.Identifier));
}
private bool IsValidForLocation(EventSet eventSet, Location location)
{
return eventSet.LocationTypeIdentifiers == null ||
eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier);
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; }
return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier);
}
private Location GetEventLocation()
{
return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation;
}
private bool CanStartEventSet(EventSet eventSet)
@@ -688,53 +744,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)
{
@@ -742,13 +795,13 @@ namespace Barotrauma
{
ev.Update(deltaTime);
}
else if (!finishedEvents.Contains(ev))
else if (ev.Prefab != null && !finishedEvents.Contains(ev.Prefab.Identifier))
{
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);
finishedEvents.Add(ev.Prefab.Identifier);
}
}
@@ -1,6 +1,6 @@
using System;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -14,12 +14,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 +40,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 +48,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 +80,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,11 +111,22 @@ namespace Barotrauma
public readonly bool IgnoreCoolDown;
public readonly bool IgnoreIntensity;
public readonly bool PerRuin, PerCave, PerWreck;
public readonly bool DisableInHuntingGrounds;
/// <summary>
/// If true, events from this set shouldn't be selected again as long as they remain in <see cref="LevelData.NonRepeatableEvents"/> which has a limited size.
/// Use <see cref="Unique"/> to prevent selecting the whole set again altogether.
/// </summary>
public readonly bool OncePerOutpost;
/// <summary>
/// If true, the whole set can only be selected once for a level.
/// </summary>
public readonly bool Unique;
public readonly bool DelayWhenCrewAway;
public readonly bool TriggerEventCooldown;
@@ -126,13 +138,26 @@ namespace Barotrauma
public readonly float ResetTime;
/// <summary>
/// Used to force an event set based on how many other locations have been discovered before this. (Used for campaign tutorial event sets.)
/// </summary>
public readonly int ForceAtDiscoveredNr;
/// <summary>
/// Used to force an event set based on how many other outposts have been visited before this. (Used for campaign tutorial event sets.)
/// </summary>
public readonly int ForceAtVisitedNr;
public readonly bool CampaignTutorialOnly;
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;
@@ -163,6 +188,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;
@@ -245,6 +272,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)
{
@@ -267,11 +296,21 @@ 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);
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
Unique = element.GetAttributeBool("unique", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
ResetTime = element.GetAttributeFloat("resettime", 0);
CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false);
ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1);
ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1);
if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0)
{
DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior");
}
DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f);
foreach (var subElement in element.Elements())
@@ -309,15 +348,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;
}
@@ -342,8 +383,22 @@ 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)
@@ -489,6 +544,11 @@ namespace Barotrauma
}
}
public override string ToString()
{
return $"{base.ToString()} ({Identifier.Value})";
}
public override void Dispose() { }
}
}
@@ -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);
@@ -214,8 +217,9 @@ 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));
@@ -226,8 +230,16 @@ namespace Barotrauma
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true);
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos, giveTags: true);
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)
{
spawnedCharacter.GiveIdCardTags(wp);
@@ -237,7 +249,10 @@ namespace Barotrauma
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
if (allowOrderingRescuees)
{
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
}
#endif
}
@@ -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,290 @@
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 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);
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)
{
int projectileAmount = Rand.Range(3, 6);
float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f));
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();
projectile.Use();
});
}
float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition);
//the closer the sub is, more likely it is to shoot frequently
float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, dist / 15000.0f);
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);
}
}
@@ -50,13 +50,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 +64,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)
{
@@ -175,7 +174,7 @@ namespace Barotrauma
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
if (!Submarine.MainSub.AtEitherExit) { return; }
State = 2;
break;
}
@@ -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);
@@ -113,13 +118,11 @@ 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;
public readonly Location[] Locations;
@@ -307,7 +310,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))
{
@@ -378,7 +381,7 @@ namespace Barotrauma
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
// use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
@@ -386,13 +389,20 @@ namespace Barotrauma
#if CLIENT
foreach (Character character in crewCharacters)
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
character.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
character.Info?.GiveExperience(experienceGain, isMissionExperience: true);
}
#else
foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients)
{
//give the experience to the stored characterinfo if the client isn't currently controlling a character
(c.Character?.Info ?? c.CharacterInfo)?.GiveExperience(experienceGain, isMissionExperience: true);
CharacterInfo info = c.Character?.Info ?? c.CharacterInfo;
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
}
#endif
@@ -422,8 +432,7 @@ namespace Barotrauma
{
if (reputationReward.Key == "location")
{
Locations[0].Reputation.AddReputation(reputationReward.Value);
Locations[1].Reputation.AddReputation(reputationReward.Value);
Locations[0].Reputation?.AddReputation(reputationReward.Value);
}
else
{
@@ -484,7 +493,7 @@ namespace Barotrauma
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++)
@@ -504,7 +513,7 @@ namespace Barotrauma
}
else
{
location.ChangeType(LocationType.Prefabs[change.ChangeToType]);
location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]);
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
}
}
@@ -518,7 +527,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;
}
@@ -527,7 +535,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;
}
@@ -619,4 +627,16 @@ namespace Barotrauma
public Mission Mission { get; set; }
}
class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
{
public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier)
{
Value = missionExperienceGainMultiplier;
Mission = mission;
}
public float Value { get; set; }
public Mission Mission { get; set; }
}
}
@@ -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,18 @@ namespace Barotrauma
public readonly bool AllowRetry;
public readonly bool ShowInMenus, ShowStartMessage;
public readonly bool IsSideObjective;
public readonly bool AllowOtherMissionsInLevel;
public readonly bool RequireWreck, RequireRuin;
public readonly bool ShowProgressBar;
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 +150,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 +173,24 @@ 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);
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);
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 +364,7 @@ namespace Barotrauma
{
return
AllowedLocationTypes.Any(lt => lt == "any") ||
AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) ||
AllowedLocationTypes.Any(lt => lt == from.Type.Identifier);
}
@@ -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);
}
}
}
@@ -274,7 +274,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,165 @@ 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;
public enum RetrievalState
{
if (item == null)
None = 0,
PickedUp = 1,
RetrievedToSub = 2
}
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 =>
RequiredRetrievalState == RetrievalState.RetrievedToSub ?
State == RetrievalState.RetrievedToSub :
State != RetrievalState.None;
private RetrievalState state;
public RetrievalState State
{
get { return state; }
set
{
Enumerable.Empty<Vector2>();
if (value == state) { return; }
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(mission);
#endif
}
}
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 +171,227 @@ 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));
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;
}
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:
var root = target.Item?.GetRootContainer() ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
return;
target.State = Target.RetrievalState.PickedUp;
if (target.Retrieved) { State = i + 1 ; }
}
}
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
break;
case Target.RetrievalState.PickedUp:
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine;
if (parentSub != null && parentSub.Info.Type == SubmarineType.Player)
{
target.State = Target.RetrievalState.RetrievedToSub;
if (target.Retrieved) { State = i + 1; }
}
break;
}
}
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.None);
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));
}
}
}