Unstable 1.8.4.0
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Modifies the win score of a team in the PvP mode.
|
||||
/// </summary>
|
||||
class AddScoreAction : EventAction
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of a target (character) whose team the score should be given to.")]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
[Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: $"Which team's score to add to? Ignored if {nameof(TargetTag)} is set.")]
|
||||
public CharacterTeamType Team { get; set; }
|
||||
|
||||
[Serialize(1, IsPropertySaveable.Yes, description: "How much to add to the score? Can also be negative.")]
|
||||
public int Amount { get; set; }
|
||||
|
||||
public AddScoreAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (Amount == 0)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in {nameof(AddScoreAction)}, event {parentEvent.Prefab.Identifier}: score set to 0, the action will do nothing.", contentPackage: element.ContentPackage);
|
||||
}
|
||||
if (TargetTag.IsEmpty && Team == CharacterTeamType.None)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in {nameof(AddScoreAction)}, event {parentEvent.Prefab.Identifier}: neither {nameof(Team)} or {nameof(TargetTag)} is set.", contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
{
|
||||
return isFinished;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
CharacterTeamType targetTeam = CharacterTeamType.None;
|
||||
if (TargetTag.IsEmpty)
|
||||
{
|
||||
targetTeam = Team;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
if (target is Character character)
|
||||
{
|
||||
targetTeam = character.TeamID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetTeam == CharacterTeamType.None) { return; }
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.GameSession?.Missions is { } missions)
|
||||
{
|
||||
foreach (var mission in missions)
|
||||
{
|
||||
if (mission is CombatMission combatMission)
|
||||
{
|
||||
combatMission.AddToScore(targetTeam, Amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
string target = TargetTag.IsEmpty ? $"team: {Team.ColorizeObject()}" : $"target: {TargetTag}";
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(AddScoreAction)} -> ({target}, amount: {Amount.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -25,6 +25,9 @@ namespace Barotrauma
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds.")]
|
||||
public Identifier ApplyTagToHull { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target (or all targets if there's multiple) when the check succeeds.")]
|
||||
public Identifier ApplyTagToTarget { get; set; }
|
||||
|
||||
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
@@ -45,7 +48,6 @@ namespace Barotrauma
|
||||
foreach (ContentXElement subElement in conditionalElements)
|
||||
{
|
||||
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
|
||||
break;
|
||||
}
|
||||
Conditionals = conditionalList.ToImmutableArray();
|
||||
}
|
||||
@@ -85,7 +87,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var target in targets)
|
||||
{
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
ApplyTagsToTarget(target);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -96,12 +98,21 @@ namespace Barotrauma
|
||||
{
|
||||
if (ConditionalsMatch(target))
|
||||
{
|
||||
ApplyTagsToTarget(target);
|
||||
success = true;
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void ApplyTagsToTarget(ISerializableEntity target)
|
||||
{
|
||||
if (!ApplyTagToTarget.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToTarget, target as Entity);
|
||||
}
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ConditionalsMatch(ISerializableEntity target)
|
||||
|
||||
@@ -200,6 +200,10 @@ namespace Barotrauma
|
||||
{
|
||||
condition = $"{value1.ColorizeObject()} {Operator.ColorizeObject()} {value2.ColorizeObject()}";
|
||||
}
|
||||
else if (!Identifier.IsEmpty)
|
||||
{
|
||||
condition = $"{Identifier} {Condition}".ColorizeObject();
|
||||
}
|
||||
|
||||
return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckDataAction)} -> (Data: {Identifier.ColorizeObject()}, Success: {succeeded.ColorizeObject()}, Expression: {condition})";
|
||||
}
|
||||
|
||||
@@ -66,13 +66,12 @@ namespace Barotrauma
|
||||
|
||||
public CheckItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers();
|
||||
itemTags = ItemTags.Split(",").ToIdentifiers();
|
||||
itemIdentifierSplit = ItemIdentifiers.ToIdentifiers().ToArray();
|
||||
itemTags = ItemTags.ToIdentifiers().ToArray();
|
||||
var conditionalList = new List<PropertyConditional>();
|
||||
foreach (ContentXElement subElement in element.GetChildElements("conditional"))
|
||||
{
|
||||
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
|
||||
break;
|
||||
}
|
||||
conditionals = conditionalList;
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!AllowSameEntity && entity == target) { continue; }
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; }
|
||||
if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing))
|
||||
if (ISpatialEntity.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing))
|
||||
{
|
||||
if (!ApplyTagToEntity.IsEmpty)
|
||||
{
|
||||
|
||||
+55
-11
@@ -84,6 +84,8 @@ namespace Barotrauma
|
||||
//an identifier the server uses to identify which ConversationAction a client is responding to
|
||||
public readonly UInt16 Identifier;
|
||||
|
||||
private float startDelay;
|
||||
|
||||
private int selectedOption = -1;
|
||||
private bool dialogOpened = false;
|
||||
|
||||
@@ -113,13 +115,15 @@ namespace Barotrauma
|
||||
Text = elem.GetAttributeString("tag", string.Empty);
|
||||
textElement = elem;
|
||||
}
|
||||
}
|
||||
if (element.GetChildElement("Replace") != null)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
|
||||
$" - unrecognized child element \"Replace\".",
|
||||
contentPackage: element.ContentPackage);
|
||||
else
|
||||
{
|
||||
string thisName = nameof(ConversationAction);
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in {thisName} in the event \"{parentEvent.Prefab.Identifier}\"" +
|
||||
$" - unrecognized child element \"{elem.Name}\". If it's an action intended to execute after the {thisName}, " +
|
||||
$"it should be after the {thisName}, not inside it.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +169,11 @@ 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)
|
||||
{
|
||||
DebugConsole.Log($"Conversation {ParentEvent.Prefab.Identifier} finished, communicating to clients...");
|
||||
ServerWrite(Speaker, c, interrupt);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
ResetSpeaker();
|
||||
@@ -209,6 +217,16 @@ namespace Barotrauma
|
||||
Speaker = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retriggers the conversation after the specified delay.
|
||||
/// </summary>
|
||||
public void RetriggerAfter(float delay)
|
||||
{
|
||||
startDelay = delay;
|
||||
dialogOpened = false;
|
||||
selectedOption = -1;
|
||||
}
|
||||
|
||||
public override bool SetGoToTarget(string goTo)
|
||||
{
|
||||
selectedOption = -1;
|
||||
@@ -238,20 +256,33 @@ namespace Barotrauma
|
||||
{
|
||||
humanAI.ClearForcedOrder();
|
||||
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
|
||||
if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
|
||||
if (prevGotoObjective != null && !prevGotoObjective.Abandon) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
|
||||
humanAI.ObjectiveManager.SortObjectives();
|
||||
}
|
||||
}
|
||||
|
||||
public int[] GetEndingOptions()
|
||||
{
|
||||
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
|
||||
List<int> endings = Options
|
||||
.Where(group =>
|
||||
group.EndConversation ||
|
||||
//no actions = safe to assume this must end the conversation
|
||||
!group.Actions.Any() ||
|
||||
//no follow-up conversation and a goto makes the event jump somewhere else
|
||||
//we cannot easily determine whether that goto will lead to a follow-up conversation,
|
||||
//so it's safest to close this conversation to prevent it from getting stuck (the potential follow-up will open a new one)
|
||||
(group.Actions.None(a => a is ConversationAction) && group.Actions.Any(a => a is GoTo { EndConversation: true })))
|
||||
.Select(group => Options.IndexOf(group))
|
||||
.ToList();
|
||||
if (!ContinueConversation) { endings.Add(-1); }
|
||||
return endings.ToArray();
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
startDelay -= deltaTime;
|
||||
if (startDelay > 0) { return; }
|
||||
|
||||
if (interrupt)
|
||||
{
|
||||
Interrupted?.Update(deltaTime);
|
||||
@@ -388,9 +419,22 @@ namespace Barotrauma
|
||||
{
|
||||
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
|
||||
if (!targets.Any() || IsBlockedByAnotherConversation(targets, BlockOtherConversationsDuration)) { return; }
|
||||
//some specific character tried to start the convo, but not included in the targets for this conversation -> disallow
|
||||
if (targetCharacter != null && !targets.Contains(targetCharacter)) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
#if SERVER
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
//conversation targeted to everyone, but no-one present yet who could potentially hear it -> don't start yet
|
||||
UpdateIgnoredClients();
|
||||
if (GameMain.NetworkMember.ConnectedClients.None(c => CanClientReceive(c))) { return; }
|
||||
}
|
||||
#endif
|
||||
if (IsBlockedByAnotherConversation(targetCharacter?.ToEnumerable(), BlockOtherConversationsDuration)) { return; }
|
||||
}
|
||||
|
||||
if (targetCharacter != null && IsBlockedByAnotherConversation(targetCharacter.ToEnumerable(), 0.1f)) { return; }
|
||||
|
||||
if (speaker?.AIController is HumanAIController humanAI)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,10 @@ namespace Barotrauma
|
||||
{
|
||||
public string Text;
|
||||
public List<EventAction> Actions;
|
||||
/// <summary>
|
||||
/// Should this option end the conversation (closing the conversation prompt?). By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation.
|
||||
/// But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, and by default leaves it open.
|
||||
/// </summary>
|
||||
public bool EndConversation;
|
||||
|
||||
private int currentSubAction = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Barotrauma
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes the event jump to a <see cref="Label"/> somewhere else in the event.
|
||||
@@ -11,6 +11,9 @@ namespace Barotrauma
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "How many times can this GoTo action be repeated? Can be used to make some parts of an event repeat a limited number of times. If negative or zero, there's no limit.")]
|
||||
public int MaxTimes { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "By default, jumping to another part in the event closes the active conversation prompt. Use this if if you want to keep it open instead.")]
|
||||
public bool EndConversation { get; set; }
|
||||
|
||||
private int counter;
|
||||
|
||||
public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -213,7 +213,7 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier())))
|
||||
if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains(Tags.AnyOutpost)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Barotrauma
|
||||
namespace Barotrauma
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
@@ -31,6 +31,11 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
if (Operation == OperationType.Add && State == 0)
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
@@ -55,7 +60,7 @@ namespace Barotrauma
|
||||
mission.State = State;
|
||||
break;
|
||||
case OperationType.Add:
|
||||
mission.State += 1;
|
||||
mission.State += State;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-23
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -42,13 +42,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
bool isPlayerTeam = TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
|
||||
bool isPlayerTeam = TeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2;
|
||||
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
|
||||
foreach (var npc in affectedNpcs)
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).OfType<Character>().ToList();
|
||||
foreach (Character npc in affectedNpcs)
|
||||
{
|
||||
// characters will still remain on friendlyNPC team for rest of the tick
|
||||
npc.SetOriginalTeam(TeamID);
|
||||
npc.SetOriginalTeamAndChangeTeam(TeamID);
|
||||
foreach (Item item in npc.Inventory.AllItems)
|
||||
{
|
||||
var idCard = item.GetComponent<Items.Components.IdCard>();
|
||||
@@ -61,26 +61,43 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (AddToCrew && isPlayerTeam)
|
||||
if (GameMain.GameSession.CrewManager is CrewManager crewManager)
|
||||
{
|
||||
npc.Info.StartItemsGiven = true;
|
||||
GameMain.GameSession.CrewManager.AddCharacter(npc);
|
||||
ChangeItemTeam(Submarine.MainSub, true);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
if (AddToCrew && isPlayerTeam)
|
||||
{
|
||||
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 == TeamID);
|
||||
ChangeItemTeam(sub, false);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems));
|
||||
if (npc.Info is CharacterInfo info)
|
||||
{
|
||||
info.StartItemsGiven = true;
|
||||
crewManager.AddCharacter(npc);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Attempted to change the team of a character ({npc.Name}) that doesn't have Character Info. Can't add to the crew.");
|
||||
}
|
||||
ChangeItemTeam(Submarine.MainSub ?? Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID), allowStealing: true);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.FindAllItems(recursive: true)));
|
||||
}
|
||||
}
|
||||
else if (RemoveFromCrew && npc.TeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2)
|
||||
{
|
||||
if (npc.Info is CharacterInfo info)
|
||||
{
|
||||
info.StartItemsGiven = true;
|
||||
crewManager.RemoveCharacter(npc, removeInfo: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Attempted to change the team of a character ({npc.Name}) that doesn't have Character Info. Can't remove from the crew.");
|
||||
}
|
||||
Submarine 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(TeamID, npc.Inventory.FindAllItems(recursive: true)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChangeItemTeam(Submarine sub, bool allowStealing)
|
||||
@@ -98,7 +115,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
WayPoint subWaypoint =
|
||||
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info.Job?.Prefab) ??
|
||||
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info?.Job?.Prefab) ??
|
||||
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human);
|
||||
if (subWaypoint != null)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -22,6 +23,20 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop following the target when the event resets?")]
|
||||
public bool AbandonOnReset { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
|
||||
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
|
||||
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
|
||||
"A priority of 60 would make the objective work like a lowest priority order." +
|
||||
"So, if we'll want the character to follow, but still be able to find safety, defend themselves when attacked, or flee from dangers," +
|
||||
"it's better to use e.g. 70 instead of 100.")]
|
||||
public float Priority
|
||||
{
|
||||
get => _priority;
|
||||
set => _priority = Math.Clamp(value, AIObjectiveManager.LowestOrderPriority, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
|
||||
private float _priority;
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
@@ -39,7 +54,7 @@ namespace Barotrauma
|
||||
if (target == null) { return; }
|
||||
|
||||
int targetCount = 0;
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character);
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).OfType<Character>();
|
||||
foreach (var npc in affectedNpcs)
|
||||
{
|
||||
if (npc.Removed) { continue; }
|
||||
@@ -49,7 +64,7 @@ namespace Barotrauma
|
||||
{
|
||||
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
|
||||
{
|
||||
OverridePriority = 100.0f,
|
||||
OverridePriority = Priority,
|
||||
IsFollowOrder = true
|
||||
};
|
||||
humanAiController.ObjectiveManager.AddObjective(newObjective);
|
||||
|
||||
+16
-4
@@ -2,6 +2,7 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -31,8 +32,19 @@ namespace Barotrauma
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs the action can target. For example, you could only make a specific number of security officers man a periscope.")]
|
||||
public int MaxTargets { get; set; }
|
||||
|
||||
[Serialize(100, IsPropertySaveable.Yes, description: "Priority of operating the item (0-100). Higher values will make the AI prefer operating the item over other orders (priority 60-70) or e.g. reacting to emergencies (priority 90).")]
|
||||
public int Priority { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
|
||||
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
|
||||
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
|
||||
"A priority of 60 would make the objective work like a lowest priority order." +
|
||||
"So, if we'll want the character to operate the item, but still be able to find safety, defend themselves when attacked, or flee from dangers," +
|
||||
"it's better to use e.g. 70 instead of 100.")]
|
||||
public float Priority
|
||||
{
|
||||
get => _priority;
|
||||
set => _priority = Math.Clamp(value, AIObjectiveManager.LowestOrderPriority, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
private float _priority;
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop operating the item when the event resets?")]
|
||||
public bool AbandonOnReset { get; set; }
|
||||
@@ -86,7 +98,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var objective in humanAiController.ObjectiveManager.Objectives)
|
||||
{
|
||||
if (objective is AIObjectiveOperateItem operateItemObjective && operateItemObjective.OperateTarget == target)
|
||||
if (objective is AIObjectiveOperateItem operateItemObjective && operateItemObjective.Component.Item == target)
|
||||
{
|
||||
objective.Abandon = true;
|
||||
}
|
||||
@@ -115,7 +127,7 @@ namespace Barotrauma
|
||||
if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; }
|
||||
foreach (var operateItemObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveOperateItem>())
|
||||
{
|
||||
if (operateItemObjective.OperateTarget == target)
|
||||
if (operateItemObjective.Component.Item == target)
|
||||
{
|
||||
operateItemObjective.Abandon = true;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -13,6 +14,20 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop waiting?")]
|
||||
public bool Wait { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
|
||||
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
|
||||
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
|
||||
"A priority of 60 would make the objective work like a lowest priority order." +
|
||||
"So, if we'll want the character to wait, but still be able to find safety, defend themselves when attacked, or flee from dangers," +
|
||||
"it's better to use e.g. 70 instead of 100.")]
|
||||
public float Priority
|
||||
{
|
||||
get => _priority;
|
||||
set => _priority = Math.Clamp(value, AIObjectiveManager.LowestOrderPriority, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
|
||||
private float _priority;
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
@@ -25,7 +40,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character);
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).OfType<Character>();
|
||||
|
||||
foreach (var npc in affectedNpcs)
|
||||
{
|
||||
@@ -38,7 +53,7 @@ namespace Barotrauma
|
||||
AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true)
|
||||
{
|
||||
FaceTargetOnCompleted = false,
|
||||
OverridePriority = 100.0f,
|
||||
OverridePriority = Priority,
|
||||
SourceEventAction = this,
|
||||
IsWaitOrder = true,
|
||||
CloseEnough = 100
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Barotrauma
|
||||
{
|
||||
ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty));
|
||||
}
|
||||
itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet();
|
||||
itemIdentifierSplit = ItemIdentifiers.ToIdentifiers().ToImmutableHashSet();
|
||||
}
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace Barotrauma
|
||||
MainSub,
|
||||
Outpost,
|
||||
MainPath,
|
||||
Cave,
|
||||
AbyssCave,
|
||||
Ruin,
|
||||
Wreck,
|
||||
BeaconStation,
|
||||
@@ -38,6 +40,9 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item to spawn.")]
|
||||
public Identifier ItemIdentifier { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to spawn.")]
|
||||
public Identifier ItemTag { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
@@ -67,6 +72,9 @@ namespace Barotrauma
|
||||
[Serialize(1, IsPropertySaveable.Yes, description: "Number of entities to spawn.")]
|
||||
public int Amount { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the item be spawned even if the target inventory is full (just spawning it at the position of the target)? Only valid if spawning an item in an inventory.")]
|
||||
public bool SpawnIfInventoryFull { get; set; }
|
||||
|
||||
[Serialize(100.0f, IsPropertySaveable.Yes, description: "Random offset to add to the spawn position.")]
|
||||
public float Offset { get; set; }
|
||||
|
||||
@@ -94,6 +102,9 @@ namespace Barotrauma
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "If disabled, the action will choose a spawn position away from players' views if one is available.")]
|
||||
public bool AllowInPlayerView { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue even if the entity failed to spawn for whatever reason?")]
|
||||
public bool ContinueIfFailedToSpawn { get; set; }
|
||||
|
||||
private bool spawned;
|
||||
private Entity spawnedEntity;
|
||||
|
||||
@@ -115,9 +126,9 @@ namespace Barotrauma
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
{
|
||||
if (spawnedEntity != null)
|
||||
if (spawnedEntity != null || ContinueIfFailedToSpawn)
|
||||
{
|
||||
return true;
|
||||
return spawned;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -157,7 +168,7 @@ namespace Barotrauma
|
||||
logError: false);
|
||||
}
|
||||
|
||||
humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true);
|
||||
humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true, contentPackageToLogInError: ParentEvent.Prefab.ContentPackage);
|
||||
|
||||
if (humanPrefab != null)
|
||||
{
|
||||
@@ -176,7 +187,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (newCharacter == null) { return; }
|
||||
newCharacter.HumanPrefab = humanPrefab;
|
||||
newCharacter.TeamID = TeamID;
|
||||
//don't set the TeamID directly: we want to leave the character's original team untouched,
|
||||
//so they can behave offensively (and otherwise act "normally") if we spawn them in a hostile team inside a sub/outpost that doesn't belong to that team
|
||||
|
||||
//process the team change immediately in case the character is killed or made unconscious by the event (in which case the team change would not be processed)
|
||||
newCharacter.SetOriginalTeamAndChangeTeam(TeamID, processImmediately: true);
|
||||
newCharacter.EnableDespawn = false;
|
||||
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
|
||||
if (LootingIsStealing)
|
||||
@@ -233,74 +248,82 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!ItemIdentifier.IsEmpty)
|
||||
else if (!ItemIdentifier.IsEmpty || !ItemTag.IsEmpty)
|
||||
{
|
||||
if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab)
|
||||
ItemPrefab itemPrefab = null;
|
||||
if (!ItemIdentifier.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)",
|
||||
contentPackage: ParentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Inventory spawnInventory = null;
|
||||
if (!TargetInventory.IsEmpty)
|
||||
itemPrefab = MapEntityPrefab.FindByIdentifier(ItemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
var targets = ParentEvent.GetTargets(TargetInventory);
|
||||
if (targets.Any())
|
||||
{
|
||||
var target = targets.First(t => t is Item || t is Character);
|
||||
if (target is Character character)
|
||||
{
|
||||
spawnInventory = character.Inventory;
|
||||
}
|
||||
else if (target is Item item)
|
||||
{
|
||||
spawnInventory = item.OwnInventory;
|
||||
}
|
||||
}
|
||||
DebugConsole.ThrowError($"Error in SpawnAction (item prefab \"{ItemIdentifier}\" not found)",
|
||||
contentPackage: ParentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
else if (!ItemTag.IsEmpty)
|
||||
{
|
||||
itemPrefab = ItemPrefab.Prefabs.Where(ip => ip.Tags.Contains(ItemTag)).GetRandom(Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
if (spawnInventory == null)
|
||||
Inventory spawnInventory = null;
|
||||
if (!TargetInventory.IsEmpty)
|
||||
{
|
||||
var targets = ParentEvent.GetTargets(TargetInventory);
|
||||
if (targets.Any())
|
||||
{
|
||||
var target = targets.First(t => t is Item || t is Character);
|
||||
if (target is Character character)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.",
|
||||
contentPackage: ParentEvent.Prefab.ContentPackage);
|
||||
spawnInventory = character.Inventory;
|
||||
}
|
||||
else if (target is Item item)
|
||||
{
|
||||
spawnInventory = item.OwnInventory;
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnInventory == null)
|
||||
{
|
||||
ISpatialEntity spawnPos = GetSpawnPos();
|
||||
if (spawnPos != null)
|
||||
{
|
||||
for (int i = 0; i < Amount; i++)
|
||||
{
|
||||
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
|
||||
}
|
||||
}
|
||||
DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.",
|
||||
contentPackage: ParentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
if (spawnInventory == null)
|
||||
{
|
||||
ISpatialEntity spawnPos = GetSpawnPos();
|
||||
if (spawnPos != null)
|
||||
{
|
||||
for (int i = 0; i < Amount; i++)
|
||||
{
|
||||
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned);
|
||||
|
||||
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
|
||||
}
|
||||
}
|
||||
void onSpawned(Item newItem)
|
||||
{
|
||||
if (newItem != null)
|
||||
{
|
||||
if (!TargetTag.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(TargetTag, newItem);
|
||||
}
|
||||
if (IgnoreByAI)
|
||||
{
|
||||
newItem.AddTag("ignorebyai");
|
||||
}
|
||||
}
|
||||
spawnedEntity = newItem;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < Amount; i++)
|
||||
{
|
||||
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, spawnIfInventoryFull: SpawnIfInventoryFull, onSpawned: onSpawned);
|
||||
|
||||
}
|
||||
}
|
||||
void onSpawned(Item newItem)
|
||||
{
|
||||
if (newItem != null)
|
||||
{
|
||||
if (!TargetTag.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(TargetTag, newItem);
|
||||
}
|
||||
if (IgnoreByAI)
|
||||
{
|
||||
newItem.AddTag("ignorebyai");
|
||||
}
|
||||
}
|
||||
spawnedEntity = newItem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
spawned = true;
|
||||
@@ -353,8 +376,7 @@ namespace Barotrauma
|
||||
{
|
||||
SpawnLocationType.Any => true,
|
||||
SpawnLocationType.MainSub => submarine == Submarine.MainSub,
|
||||
SpawnLocationType.NearMainSub => submarine == null,
|
||||
SpawnLocationType.MainPath => submarine == null,
|
||||
SpawnLocationType.NearMainSub or SpawnLocationType.MainPath or SpawnLocationType.Cave or SpawnLocationType.AbyssCave => submarine == null,
|
||||
SpawnLocationType.Outpost => submarine is { Info.IsOutpost: true },
|
||||
SpawnLocationType.Wreck => submarine is { Info.IsWreck: true },
|
||||
SpawnLocationType.Ruin => submarine is { Info.IsRuin: true },
|
||||
@@ -443,10 +465,25 @@ namespace Barotrauma
|
||||
return potentialSpawnPoints.GetRandomUnsynced();
|
||||
}
|
||||
|
||||
if (spawnLocation == SpawnLocationType.MainPath || spawnLocation == SpawnLocationType.NearMainSub)
|
||||
switch (spawnLocation)
|
||||
{
|
||||
validSpawnPoints = validSpawnPoints.Where(p =>
|
||||
Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition)));
|
||||
case SpawnLocationType.MainPath:
|
||||
case SpawnLocationType.NearMainSub:
|
||||
validSpawnPoints = validSpawnPoints.Where(p =>
|
||||
Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition)));
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
validSpawnPoints = validSpawnPoints.Where(p =>
|
||||
p.WorldPosition.Y > Level.Loaded.AbyssStart &&
|
||||
p.Cave == null && p.Ruin == null);
|
||||
}
|
||||
break;
|
||||
case SpawnLocationType.Cave:
|
||||
validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y > Level.Loaded.AbyssStart && p.Cave != null);
|
||||
break;
|
||||
case SpawnLocationType.AbyssCave:
|
||||
validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y < Level.Loaded.AbyssStart && p.Cave != null);
|
||||
break;
|
||||
}
|
||||
|
||||
//avoid using waypoints if there's any actual spawnpoints available
|
||||
|
||||
@@ -11,9 +11,10 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
class TagAction : EventAction
|
||||
{
|
||||
public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 }
|
||||
public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8, Enemy = 16, Ruin = 32 }
|
||||
public enum CharacterTeam { Any = 0, None = 1, Team1 = 2, Team2 = 4, FriendlyNPC = 8 }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag].")]
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag], speciesname:[id].")]
|
||||
public string Criteria { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "The tag to apply to the target.")]
|
||||
@@ -22,6 +23,9 @@ namespace Barotrauma
|
||||
[Serialize(SubType.Any, IsPropertySaveable.Yes, description: "The type of submarine the target needs to be in.")]
|
||||
public SubType SubmarineType { get; set; }
|
||||
|
||||
[Serialize(CharacterTeam.Any, IsPropertySaveable.Yes, description: "The team the target needs to be on.")]
|
||||
public CharacterTeam Team { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, "If set, the target must be in an outpost module that has this tag.")]
|
||||
public Identifier RequiredModuleTag { get; set; }
|
||||
|
||||
@@ -34,6 +38,9 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If there are multiple matching targets, should all of them be tagged or one chosen randomly?")]
|
||||
public bool ChooseRandom { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "If choosing a random target, targets with this tag can optionally be excluded.")]
|
||||
public Identifier ChooseRandomExcludingTag { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue if the TagAction can't find any valid targets?")]
|
||||
public bool ContinueIfNoTargetsFound { get; set; }
|
||||
|
||||
@@ -78,6 +85,7 @@ namespace Barotrauma
|
||||
("hullname", TagHullsByName),
|
||||
("submarine", TagSubmarinesByType),
|
||||
("eventtag", TagByEventTag),
|
||||
("speciesname", TagBySpeciesName)
|
||||
}.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary();
|
||||
}
|
||||
|
||||
@@ -87,9 +95,16 @@ namespace Barotrauma
|
||||
}
|
||||
public override void Reset()
|
||||
{
|
||||
taggingDone = false;
|
||||
cantFindTargets = false;
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
private void TagBySpeciesName(Identifier speciesName)
|
||||
{
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.SpeciesName == speciesName && CharacterTeamMatches(c)));
|
||||
}
|
||||
|
||||
private void TagByEventTag(Identifier eventTag)
|
||||
{
|
||||
AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => MatchesRequirements(t)));
|
||||
@@ -100,7 +115,7 @@ namespace Barotrauma
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters));
|
||||
e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && CharacterTeamMatches(c));
|
||||
}
|
||||
|
||||
private void TagTraitors()
|
||||
@@ -151,7 +166,7 @@ namespace Barotrauma
|
||||
|
||||
private void TagHumansByIdentifier(Identifier identifier)
|
||||
{
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier));
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier && CharacterTeamMatches(c)));
|
||||
}
|
||||
|
||||
private void TagHumansByTag(Identifier tag)
|
||||
@@ -161,7 +176,7 @@ namespace Barotrauma
|
||||
|
||||
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
|
||||
{
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier)));
|
||||
AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier) && CharacterTeamMatches(c)));
|
||||
}
|
||||
|
||||
private void TagStructuresByIdentifier(Identifier identifier)
|
||||
@@ -233,7 +248,7 @@ namespace Barotrauma
|
||||
|
||||
private bool MatchesRequirements(Entity e)
|
||||
{
|
||||
return ModuleTagMatches(e) && SubmarineTypeMatches(e.Submarine);
|
||||
return ModuleTagMatches(e) && SubmarineTypeMatches(e as Submarine ?? e.Submarine);
|
||||
}
|
||||
|
||||
private bool ModuleTagMatches(Entity e)
|
||||
@@ -267,6 +282,23 @@ namespace Barotrauma
|
||||
return hull != null && hull.OutpostModuleTags.Contains(RequiredModuleTag);
|
||||
}
|
||||
|
||||
private bool CharacterTeamMatches(Character character)
|
||||
{
|
||||
if (Team == CharacterTeam.Any) { return true; }
|
||||
switch (Team)
|
||||
{
|
||||
case CharacterTeam.None:
|
||||
return character.TeamID == CharacterTeamType.None;
|
||||
case CharacterTeam.Team1:
|
||||
return character.TeamID == CharacterTeamType.Team1;
|
||||
case CharacterTeam.Team2:
|
||||
return character.TeamID == CharacterTeamType.Team2;
|
||||
case CharacterTeam.FriendlyNPC:
|
||||
return character.TeamID == CharacterTeamType.FriendlyNPC;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool SubmarineTypeMatches(Submarine sub)
|
||||
{
|
||||
@@ -280,7 +312,7 @@ namespace Barotrauma
|
||||
switch (sub.Info.Type)
|
||||
{
|
||||
case Barotrauma.SubmarineType.Player:
|
||||
return submarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle;
|
||||
return submarineType.HasFlag(SubType.Player) && !sub.IsRespawnShuttle;
|
||||
case Barotrauma.SubmarineType.Outpost:
|
||||
case Barotrauma.SubmarineType.OutpostModule:
|
||||
return submarineType.HasFlag(SubType.Outpost);
|
||||
@@ -288,6 +320,10 @@ namespace Barotrauma
|
||||
return submarineType.HasFlag(SubType.Wreck);
|
||||
case Barotrauma.SubmarineType.BeaconStation:
|
||||
return submarineType.HasFlag(SubType.BeaconStation);
|
||||
case Barotrauma.SubmarineType.EnemySubmarine:
|
||||
return submarineType.HasFlag(SubType.Enemy);
|
||||
case Barotrauma.SubmarineType.Ruin:
|
||||
return submarineType.HasFlag(SubType.Ruin);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -357,6 +393,11 @@ namespace Barotrauma
|
||||
|
||||
private void TagRandom(Identifier tag, IEnumerable<Entity> entities)
|
||||
{
|
||||
if (!ChooseRandomExcludingTag.IsEmpty)
|
||||
{
|
||||
var excludedTargets = ParentEvent.GetTargets(ChooseRandomExcludingTag);
|
||||
entities = entities.Except(excludedTargets);
|
||||
}
|
||||
if (entities.None())
|
||||
{
|
||||
cantFindTargets = true;
|
||||
@@ -414,7 +455,7 @@ namespace Barotrauma
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()})";
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()}, Team: {Team.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -56,7 +56,16 @@ namespace Barotrauma
|
||||
|
||||
private float distance;
|
||||
|
||||
public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (element.GetAttribute(nameof(TagAction.IgnoreIncapacitatedCharacters)) != null)
|
||||
{
|
||||
DebugConsole.AddWarning(
|
||||
$"Potential error in {nameof(TriggerAction)}, event \"{parentEvent.Prefab.Identifier}\": "+
|
||||
$"{nameof(TagAction.IgnoreIncapacitatedCharacters)} is a property of {nameof(TagAction)}, did you mean {nameof(DisableIfTargetIncapacitated)}?",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
private bool isFinished = false;
|
||||
public override bool IsFinished(ref string goTo)
|
||||
@@ -157,13 +166,13 @@ namespace Barotrauma
|
||||
Item item = null;
|
||||
if (e1 is Character char1)
|
||||
{
|
||||
if (char1.IsBot)
|
||||
{
|
||||
npc ??= char1;
|
||||
if (char1.IsPlayer)
|
||||
{
|
||||
player = char1;
|
||||
}
|
||||
else
|
||||
{
|
||||
player = char1;
|
||||
else
|
||||
{
|
||||
npc ??= char1;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -172,13 +181,13 @@ namespace Barotrauma
|
||||
}
|
||||
if (e2 is Character char2)
|
||||
{
|
||||
if (char2.IsBot)
|
||||
{
|
||||
npc ??= char2;
|
||||
}
|
||||
else
|
||||
if (char2.IsPlayer)
|
||||
{
|
||||
player = char2;
|
||||
player = char2;
|
||||
}
|
||||
else
|
||||
{
|
||||
npc ??= char2;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -190,6 +199,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (npc != null)
|
||||
{
|
||||
if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc))
|
||||
{
|
||||
npcsOrItems.Add(npc);
|
||||
}
|
||||
if (npc.CampaignInteractionType == CampaignMode.InteractionType.Talk)
|
||||
{
|
||||
//if the NPC has a conversation available, don't assign the trigger until the conversation is done
|
||||
@@ -197,19 +210,26 @@ namespace Barotrauma
|
||||
}
|
||||
else if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
|
||||
{
|
||||
if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc))
|
||||
{
|
||||
npcsOrItems.Add(npc);
|
||||
}
|
||||
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
|
||||
npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated;
|
||||
#if CLIENT
|
||||
npc.SetCustomInteract(
|
||||
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
|
||||
(Character npc, Character interactor) =>
|
||||
{
|
||||
//the first character in the CustomInteract callback is always the NPC and the 2nd the character who interacted with it
|
||||
//but the TriggerAction can configure the 1st and 2nd entity in either order,
|
||||
//let's make sure we pass the NPC and the interactor in the intended order
|
||||
if (e1 == npc && targets2.Contains(interactor))
|
||||
{
|
||||
Trigger(npc, interactor);
|
||||
}
|
||||
else if (targets1.Contains(interactor) && e2 == npc)
|
||||
{
|
||||
Trigger(interactor, npc);
|
||||
}
|
||||
},
|
||||
#if CLIENT
|
||||
TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
|
||||
#else
|
||||
npc.SetCustomInteract(
|
||||
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
|
||||
TextManager.Get("CampaignInteraction.Talk"));
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData());
|
||||
#endif
|
||||
@@ -339,7 +359,7 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is EnemyAIController enemyAI && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack))
|
||||
else if (c.AIController is EnemyAIController { State: AIState.Aggressive or AIState.Attack } enemyAI)
|
||||
{
|
||||
if (enemyAI.SelectedAiTarget?.Entity == character || c.CurrentHull == character.CurrentHull)
|
||||
{
|
||||
@@ -401,10 +421,18 @@ namespace Barotrauma
|
||||
{
|
||||
if (TargetModuleType.IsEmpty)
|
||||
{
|
||||
string targetStr = "none";
|
||||
if (npcsOrItems.Any())
|
||||
{
|
||||
targetStr = string.Join(", ",
|
||||
npcsOrItems.Select(npcOrItem =>
|
||||
npcOrItem.TryGet(out Character character) ? character.Name : (npcOrItem.TryGet(out Item item) ? item.Name : "none")));
|
||||
}
|
||||
|
||||
return
|
||||
$"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" +
|
||||
(WaitForInteraction ?
|
||||
$"Selected non-player target: {(npcsOrItems?.ToString() ?? "<null>").ColorizeObject()}, " :
|
||||
$"Selected non-player target: {targetStr.ColorizeObject()}, " :
|
||||
$"Distance: {((int)distance).ColorizeObject()}, ") +
|
||||
$"Radius: {Radius.ColorizeObject()}, " +
|
||||
$"TargetTags: {Target1Tag.ColorizeObject()}, " +
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Triggers another scripted event.
|
||||
/// Triggers another event (can also trigger things other than scripted events, for example monster events).
|
||||
/// </summary>
|
||||
class TriggerEventAction : EventAction
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the event to trigger.")]
|
||||
public Identifier Identifier { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the event to trigger.")]
|
||||
public Identifier EventTag { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the event will trigger at the beginning of the next round. Useful for e.g. triggering some scripted event in the outpost after you finish a mission.")]
|
||||
public bool NextRound { get; set; }
|
||||
|
||||
@@ -36,13 +39,8 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var eventPrefab = EventSet.GetEventPrefab(Identifier);
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.",
|
||||
contentPackage: ParentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
else
|
||||
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(Identifier, EventTag, ParentEvent.Prefab.ContentPackage);
|
||||
if (eventPrefab != null)
|
||||
{
|
||||
var ev = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
|
||||
if (ev != null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -9,6 +10,13 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
class UnlockPathAction : EventAction
|
||||
{
|
||||
private static readonly HashSet<LocationConnection> pathsUnlockedThisRound = new HashSet<LocationConnection>();
|
||||
|
||||
public static void ResetPathsUnlockedThisRound()
|
||||
{
|
||||
pathsUnlockedThisRound.Clear();
|
||||
}
|
||||
|
||||
public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
|
||||
private bool isFinished = false;
|
||||
@@ -32,6 +40,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!connection.Locked) { continue; }
|
||||
connection.Locked = false;
|
||||
pathsUnlockedThisRound.Add(connection);
|
||||
#if SERVER
|
||||
NotifyUnlock(connection);
|
||||
#else
|
||||
@@ -50,17 +59,30 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
private void NotifyUnlock(LocationConnection connection)
|
||||
public static void NotifyPathsUnlockedThisRound(Client client)
|
||||
{
|
||||
foreach (LocationConnection connection in pathsUnlockedThisRound)
|
||||
{
|
||||
NotifyUnlock(connection, client);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyUnlock(LocationConnection connection)
|
||||
{
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
IWriteMessage outmsg = new WriteOnlyMessage();
|
||||
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
|
||||
outmsg.WriteByte((byte)EventManager.NetworkEventType.UNLOCKPATH);
|
||||
outmsg.WriteUInt16((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection));
|
||||
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
NotifyUnlock(connection, client);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyUnlock(LocationConnection connection, Client client)
|
||||
{
|
||||
IWriteMessage outmsg = new WriteOnlyMessage();
|
||||
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
|
||||
outmsg.WriteByte((byte)EventManager.NetworkEventType.UNLOCKPATH);
|
||||
outmsg.WriteUInt16((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection));
|
||||
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
@@ -12,6 +12,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
class WaitForItemUsedAction : EventAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Counter used to ensure we have a unique identifier to use for the ItemComponent.OnUsed event
|
||||
/// </summary>
|
||||
private static int IdCounter;
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must be used. Note that the item needs to have been tagged by the event - this does not refer to the tags that can be set per-item in the sub editor.")]
|
||||
public Identifier ItemTag { get; set; }
|
||||
|
||||
@@ -50,7 +55,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (onUseEventIdentifier.IsEmpty)
|
||||
{
|
||||
onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString()).ToIdentifier();
|
||||
onUseEventIdentifier = (ParentEvent.Prefab.Identifier + ParentEvent.Actions.IndexOf(this).ToString() + IdCounter).ToIdentifier();
|
||||
IdCounter++;
|
||||
}
|
||||
return onUseEventIdentifier;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ namespace Barotrauma
|
||||
activeEvents.Clear();
|
||||
#if SERVER
|
||||
MissionAction.ResetMissionsUnlockedThisRound();
|
||||
UnlockPathAction.ResetPathsUnlockedThisRound();
|
||||
#endif
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
totalPathLength = 0.0f;
|
||||
@@ -180,11 +181,46 @@ namespace Barotrauma
|
||||
random = new MTRandom(RandomSeed);
|
||||
|
||||
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
|
||||
EventSet initialEventSet = SelectRandomEvents(
|
||||
EventSet.Prefabs.ToList(),
|
||||
requireCampaignSet: playingCampaign,
|
||||
random: random);
|
||||
|
||||
//ensure that the sets that have been configured to be always selected get selected if there's any available
|
||||
EventSet initialEventSet = null;
|
||||
EventSet additiveSet = null;
|
||||
var selectAlwaysEventSets = GetAllowedEventSets(EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign).Where(s => s.SelectAlways);
|
||||
foreach (var eventSet in selectAlwaysEventSets)
|
||||
{
|
||||
if (eventSet.GetCommonness(level) <= 0.0f)
|
||||
{
|
||||
//you might be wondering why an event set would be configured to SelectAlways, but have a commonness of 0:
|
||||
//the set might have a non-zero commonness in some other biome or level type, but not this one
|
||||
continue;
|
||||
}
|
||||
if (eventSet.Additive)
|
||||
{
|
||||
additiveSet = eventSet;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (initialEventSet == null)
|
||||
{
|
||||
initialEventSet = eventSet;
|
||||
}
|
||||
else //initial set already chosen, ignore this one
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
AddSet(eventSet);
|
||||
}
|
||||
|
||||
if (initialEventSet == null)
|
||||
{
|
||||
initialEventSet = SelectRandomEvents(
|
||||
EventSet.Prefabs.ToList(),
|
||||
requireCampaignSet: playingCampaign,
|
||||
random: random);
|
||||
}
|
||||
|
||||
//we happened to choose an additive set as the initial one, choose an additive one too
|
||||
if (initialEventSet != null && initialEventSet.Additive)
|
||||
{
|
||||
additiveSet = initialEventSet;
|
||||
@@ -193,15 +229,15 @@ namespace Barotrauma
|
||||
requireCampaignSet: playingCampaign,
|
||||
random: random);
|
||||
}
|
||||
if (initialEventSet != null)
|
||||
|
||||
if (initialEventSet != null) { AddSet(initialEventSet); }
|
||||
if (additiveSet != null) { AddSet(additiveSet); }
|
||||
|
||||
void AddSet(EventSet eventSet)
|
||||
{
|
||||
pendingEventSets.Add(initialEventSet);
|
||||
CreateEvents(initialEventSet);
|
||||
}
|
||||
if (additiveSet != null)
|
||||
{
|
||||
pendingEventSets.Add(additiveSet);
|
||||
CreateEvents(additiveSet);
|
||||
if (pendingEventSets.Contains(eventSet)) { return; }
|
||||
pendingEventSets.Add(eventSet);
|
||||
CreateEvents(eventSet);
|
||||
}
|
||||
|
||||
if (level?.LevelData != null)
|
||||
@@ -223,6 +259,22 @@ namespace Barotrauma
|
||||
level.StartLocation.Connections.ForEach(c => c.Locked = false);
|
||||
}
|
||||
}
|
||||
if (GameMain.NetworkMember is not { IsClient: true } && level.StartOutpost != null)
|
||||
{
|
||||
foreach (var eventTag in level.StartOutpost.Info.TriggerOutpostMissionEvents)
|
||||
{
|
||||
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, level.StartOutpost.ContentPackage);
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Outpost {level.StartOutpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: level.StartOutpost.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newEvent = eventPrefab.CreateInstance(RandomSeed);
|
||||
ActivateEvent(newEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RegisterNonRepeatableChildEvents(initialEventSet);
|
||||
void RegisterNonRepeatableChildEvents(EventSet eventSet)
|
||||
@@ -244,7 +296,7 @@ namespace Barotrauma
|
||||
|
||||
while (QueuedEventsForNextRound.TryDequeue(out var id))
|
||||
{
|
||||
var eventPrefab = EventSet.GetEventPrefab(id);
|
||||
var eventPrefab = EventSet.GetEventPrefab(id) ?? EventSet.GetAllEventPrefabs().Where(e => e.Tags.Contains(id)).GetRandomUnsynced();
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}.");
|
||||
@@ -421,12 +473,10 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history
|
||||
/// </summary>
|
||||
public void RegisterEventHistory(bool registerFinishedOnly = false)
|
||||
public void StoreEventDataAtRoundEnd(bool registerFinishedOnly = false)
|
||||
{
|
||||
if (level?.LevelData == null) { return; }
|
||||
|
||||
level.LevelData.EventsExhausted = !registerFinishedOnly;
|
||||
|
||||
if (level.LevelData.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
if (registerFinishedOnly)
|
||||
@@ -437,7 +487,7 @@ namespace Barotrauma
|
||||
if (parentSet == null) { continue; }
|
||||
if (parentSet.Exhaustible)
|
||||
{
|
||||
level.LevelData.EventsExhausted = true;
|
||||
level.LevelData.ExhaustEventSet(parentSet);
|
||||
}
|
||||
if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1))
|
||||
{
|
||||
@@ -484,7 +534,7 @@ namespace Barotrauma
|
||||
selectedEvents.Remove(eventSet);
|
||||
if (level == null) { return; }
|
||||
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
|
||||
if (eventSet.Exhaustible && level.LevelData.EventsExhausted) { return; }
|
||||
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
|
||||
|
||||
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
|
||||
|
||||
@@ -609,12 +659,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private EventSet SelectRandomEvents(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null, Random random = null)
|
||||
private IEnumerable<EventSet> GetAllowedEventSets(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null)
|
||||
{
|
||||
if (level == null) { return null; }
|
||||
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
if (level == null) { return Enumerable.Empty<EventSet>(); }
|
||||
|
||||
var allowedEventSets =
|
||||
var allowedEventSets =
|
||||
eventSets.Where(set => IsValidForLevel(set, level));
|
||||
|
||||
if (requireCampaignSet.HasValue)
|
||||
@@ -659,6 +708,12 @@ namespace Barotrauma
|
||||
// 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);
|
||||
}
|
||||
return allowedEventSets;
|
||||
}
|
||||
|
||||
private EventSet SelectRandomEvents(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null, Random random = null)
|
||||
{
|
||||
var allowedEventSets = GetAllowedEventSets(eventSets, requireCampaignSet);
|
||||
|
||||
if (allowedEventSets.Count() == 1)
|
||||
{
|
||||
@@ -666,6 +721,7 @@ namespace Barotrauma
|
||||
return allowedEventSets.First();
|
||||
}
|
||||
|
||||
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
|
||||
float randomNumber = (float)rand.NextDouble();
|
||||
randomNumber *= totalCommonness;
|
||||
@@ -692,6 +748,7 @@ namespace Barotrauma
|
||||
return
|
||||
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
|
||||
(e.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(e.RequiredLayer)) &&
|
||||
(e.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(e.RequiredSpawnPointTag))) &&
|
||||
!level.LevelData.NonRepeatableEvents.Contains(e.Identifier);
|
||||
}
|
||||
|
||||
@@ -704,8 +761,9 @@ namespace Barotrauma
|
||||
{
|
||||
return
|
||||
level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) &&
|
||||
level.LevelData.Type == eventSet.LevelType &&
|
||||
eventSet.LevelType.HasFlag(level.LevelData.Type) &&
|
||||
(eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) &&
|
||||
(eventSet.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(eventSet.RequiredSpawnPointTag))) &&
|
||||
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
|
||||
}
|
||||
|
||||
@@ -716,7 +774,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; }
|
||||
}
|
||||
var locationType = location.GetLocationType();
|
||||
var locationType = location.Type;
|
||||
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);
|
||||
@@ -965,9 +1023,9 @@ namespace Barotrauma
|
||||
monsterStrength += enemyAI.CombatStrength;
|
||||
}
|
||||
|
||||
if (character.CurrentHull?.Submarine?.Info != null &&
|
||||
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
|
||||
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
|
||||
if (Submarine.MainSub != null &&
|
||||
character.CurrentHull?.Submarine.Info is { Type: SubmarineType.Player } &&
|
||||
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
|
||||
{
|
||||
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
|
||||
enemyDanger += enemyAI.CombatStrength / 500.0f;
|
||||
@@ -981,8 +1039,8 @@ namespace Barotrauma
|
||||
}
|
||||
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
|
||||
{
|
||||
if (character.Submarine != null &&
|
||||
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
|
||||
if (character.Submarine != null && Submarine.MainSub != null &&
|
||||
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
|
||||
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
|
||||
{
|
||||
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
|
||||
@@ -1111,16 +1169,15 @@ namespace Barotrauma
|
||||
/// Get the entity that should be used in determining how far the player has progressed in the level.
|
||||
/// = The submarine or player character that has progressed the furthest.
|
||||
/// </summary>
|
||||
public static ISpatialEntity GetRefEntity()
|
||||
public static ISpatialEntity GetRefEntity(bool acceptRemoteControlledSubs = false)
|
||||
{
|
||||
ISpatialEntity refEntity = Submarine.MainSub;
|
||||
#if CLIENT
|
||||
if (Character.Controlled != null)
|
||||
{
|
||||
if (Character.Controlled.Submarine != null &&
|
||||
Character.Controlled.Submarine.Info.Type == SubmarineType.Player)
|
||||
if (Character.Controlled.Submarine is { Info.Type: SubmarineType.Player } playerSub)
|
||||
{
|
||||
refEntity = Character.Controlled.Submarine;
|
||||
GetRefSubForCharacter(Character.Controlled);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1128,22 +1185,44 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (refEntity == null) { return null; }
|
||||
|
||||
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (client.Character == null) { continue; }
|
||||
//only take the players inside a player sub into account.
|
||||
//Otherwise the system could be abused by for example making a respawned player wait
|
||||
//close to the destination outpost
|
||||
if (client.Character.Submarine != null &&
|
||||
client.Character.Submarine.Info.Type == SubmarineType.Player)
|
||||
GetRefSubForCharacter(client.Character);
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
void GetRefSubForCharacter(Character character)
|
||||
{
|
||||
if (character.Submarine is { Info.Type: SubmarineType.Player } playerSub)
|
||||
{
|
||||
if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
if (playerSub.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
{
|
||||
refEntity = client.Character.Submarine;
|
||||
refEntity = playerSub;
|
||||
}
|
||||
}
|
||||
if (acceptRemoteControlledSubs)
|
||||
{
|
||||
if (character.ViewTarget?.Submarine is { Info.Type: SubmarineType.Player } viewedSub)
|
||||
{
|
||||
if (viewedSub.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
{
|
||||
refEntity = viewedSub;
|
||||
}
|
||||
}
|
||||
if (character.SelectedItem?.GetComponent<Steering>()?.ControlledSub is { } controlledSub)
|
||||
{
|
||||
if (controlledSub.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
{
|
||||
refEntity = controlledSub;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return refEntity;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -11,6 +13,9 @@ namespace Barotrauma
|
||||
public readonly ContentXElement ConfigElement;
|
||||
public readonly Type EventType;
|
||||
|
||||
private readonly ImmutableHashSet<Identifier> tags;
|
||||
public ImmutableHashSet<Identifier> Tags => tags;
|
||||
|
||||
/// <summary>
|
||||
/// The probability for the event to do something if it gets selected. For example, the probability for a MonsterEvent to spawn the monster(s).
|
||||
/// </summary>
|
||||
@@ -37,6 +42,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly Identifier RequiredLayer;
|
||||
|
||||
/// <summary>
|
||||
/// If set, this spawn point tag must be present somewhere in the level.
|
||||
/// </summary>
|
||||
public readonly Identifier RequiredSpawnPointTag;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the event set can only be chosen in locations that belong to this faction.
|
||||
/// </summary>
|
||||
@@ -93,6 +103,7 @@ namespace Barotrauma
|
||||
|
||||
Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString());
|
||||
|
||||
tags = ConfigElement.GetAttributeIdentifierImmutableHashSet(nameof(tags), ImmutableHashSet<Identifier>.Empty);
|
||||
BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty);
|
||||
Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
Commonness = element.GetAttributeFloat("commonness", 1.0f);
|
||||
@@ -100,6 +111,7 @@ namespace Barotrauma
|
||||
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent));
|
||||
|
||||
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
|
||||
RequiredSpawnPointTag = element.GetAttributeIdentifier(nameof(RequiredSpawnPointTag), Identifier.Empty);
|
||||
|
||||
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
|
||||
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
|
||||
@@ -146,5 +158,39 @@ namespace Barotrauma
|
||||
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ??
|
||||
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an event prefab with the specified identifier, or if it isn't defined, a random event prefab with the specified tag.
|
||||
/// </summary>
|
||||
/// <param name="source">Which content package is trying to find the event (if any)? Only used for logging error messages.</param>
|
||||
/// <returns></returns>
|
||||
public static EventPrefab FindEventPrefab(Identifier identifier, Identifier tag, ContentPackage source)
|
||||
{
|
||||
EventPrefab eventPrefab = null;
|
||||
if (!identifier.IsEmpty)
|
||||
{
|
||||
eventPrefab = EventSet.GetEventPrefab(identifier);
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find an event prefab with the identifier {identifier}.",
|
||||
contentPackage: source);
|
||||
}
|
||||
}
|
||||
else if (!tag.IsEmpty)
|
||||
{
|
||||
eventPrefab = EventSet.GetAllEventPrefabs().Where(e => e.Tags.Contains(tag)).GetRandomUnsynced();
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find an event prefab with the tag {tag}.",
|
||||
contentPackage: source);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find an event prefab: neither an identifier or tag were defined.",
|
||||
contentPackage: source);
|
||||
}
|
||||
return eventPrefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,25 +61,46 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
|
||||
public static List<EventPrefab> GetAllEventPrefabs()
|
||||
private static readonly Dictionary<Identifier, EventPrefab> AllEventPrefabs = new Dictionary<Identifier, EventPrefab>();
|
||||
|
||||
public static IEnumerable<EventPrefab> GetAllEventPrefabs()
|
||||
{
|
||||
List<EventPrefab> eventPrefabs = EventPrefab.Prefabs.ToList();
|
||||
foreach (var eventSet in Prefabs)
|
||||
{
|
||||
AddSetEventPrefabsToList(eventPrefabs, eventSet);
|
||||
}
|
||||
return eventPrefabs;
|
||||
return AllEventPrefabs.Values;
|
||||
}
|
||||
|
||||
public static void AddSetEventPrefabsToList(List<EventPrefab> list, EventSet set)
|
||||
/// <summary>
|
||||
/// Finds all the event prefabs (both "normal prefabs" that exists by themselves, present in <see cref="EventPrefab.Prefabs"/>, and the ones that exists only inside child event sets),
|
||||
/// and adds them to <see cref="AllEventPrefabs"/>.
|
||||
/// </summary>
|
||||
public static void RefreshAllEventPrefabs()
|
||||
{
|
||||
list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs));
|
||||
foreach (var childSet in set.ChildSets) { AddSetEventPrefabsToList(list, childSet); }
|
||||
AllEventPrefabs.Clear();
|
||||
foreach (var eventPrefab in EventPrefab.Prefabs)
|
||||
{
|
||||
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
}
|
||||
foreach (var eventSet in Prefabs)
|
||||
{
|
||||
AddChildEventPrefabs(eventSet);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddChildEventPrefabs(EventSet set)
|
||||
{
|
||||
foreach (var subEventPrefabs in set.EventPrefabs)
|
||||
{
|
||||
foreach (var eventPrefab in subEventPrefabs.EventPrefabs)
|
||||
{
|
||||
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet); }
|
||||
}
|
||||
|
||||
public static EventPrefab GetEventPrefab(Identifier identifier)
|
||||
{
|
||||
return GetAllEventPrefabs().Find(prefab => prefab.Identifier == identifier);
|
||||
return AllEventPrefabs.GetValueOrDefault(identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -111,6 +132,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly Identifier RequiredLayer;
|
||||
|
||||
/// <summary>
|
||||
/// If set, this spawn point tag must be present somewhere in the level.
|
||||
/// </summary>
|
||||
public readonly Identifier RequiredSpawnPointTag;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the event set can only be chosen in locations of this type.
|
||||
/// </summary>
|
||||
@@ -207,7 +233,14 @@ namespace Barotrauma
|
||||
/// monsters in addition to the vanilla monsters spawned by vanilla sets, without you having to add your custom monsters to every single vanilla set.
|
||||
/// </summary>
|
||||
public readonly bool Additive;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This will force the game to always choose this event set if it's suitable for the current level.
|
||||
/// If the set is additive, it is guaranteed to get chosen regardless of what other sets get selected.
|
||||
/// If the set is NOT additive, the game will choose the first available non-additive set that is configured to be always selected.
|
||||
/// </summary>
|
||||
public readonly bool SelectAlways;
|
||||
|
||||
/// <summary>
|
||||
/// The commonness of the event set (i.e. how likely it is for this specific set to be chosen).
|
||||
/// </summary>
|
||||
@@ -349,7 +382,8 @@ namespace Barotrauma
|
||||
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
|
||||
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
|
||||
|
||||
Additive = element.GetAttributeBool("additive", false);
|
||||
Additive = element.GetAttributeBool(nameof(Additive), false);
|
||||
SelectAlways = element.GetAttributeBool(nameof(SelectAlways), false);
|
||||
|
||||
string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection");
|
||||
if (!Enum.TryParse(levelTypeStr, true, out LevelType))
|
||||
@@ -392,6 +426,7 @@ namespace Barotrauma
|
||||
CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false);
|
||||
|
||||
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
|
||||
RequiredSpawnPointTag = element.GetAttributeIdentifier(nameof(RequiredSpawnPointTag), Identifier.Empty);
|
||||
|
||||
ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1);
|
||||
ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1);
|
||||
|
||||
+46
-149
@@ -1,6 +1,5 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
@@ -9,10 +8,6 @@ namespace Barotrauma
|
||||
{
|
||||
partial class AbandonedOutpostMission : Mission
|
||||
{
|
||||
private readonly XElement characterConfig;
|
||||
|
||||
protected readonly List<Character> characters = new List<Character>();
|
||||
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
|
||||
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
|
||||
|
||||
@@ -27,9 +22,9 @@ namespace Barotrauma
|
||||
private const float EndDelay = 5.0f;
|
||||
private float endTimer;
|
||||
|
||||
private bool allowOrderingRescuees;
|
||||
private readonly bool allowOrderingRescuees;
|
||||
|
||||
public override bool AllowRespawn => false;
|
||||
public override bool AllowRespawning => false;
|
||||
|
||||
public override bool AllowUndocking
|
||||
{
|
||||
@@ -82,8 +77,6 @@ namespace Barotrauma
|
||||
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
|
||||
base(prefab, locations, sub)
|
||||
{
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
|
||||
allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true);
|
||||
|
||||
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
|
||||
@@ -97,8 +90,6 @@ namespace Barotrauma
|
||||
{
|
||||
failed = false;
|
||||
endTimer = 0.0f;
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
requireKill.Clear();
|
||||
requireRescue.Clear();
|
||||
items.Clear();
|
||||
@@ -165,141 +156,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitCharacters(Submarine submarine)
|
||||
{
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
|
||||
if (characterConfig != null)
|
||||
{
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
|
||||
|
||||
int defaultCount = element.GetAttributeInt("count", -1);
|
||||
if (defaultCount < 0)
|
||||
{
|
||||
defaultCount = element.GetAttributeInt("amount", 1);
|
||||
}
|
||||
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
|
||||
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
|
||||
int count = Rand.Range(min, max + 1);
|
||||
|
||||
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
|
||||
{
|
||||
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
|
||||
if (humanPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadHuman(humanPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty));
|
||||
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (characterPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadMonster(characterPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
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, spawnPointType,
|
||||
moduleFlags ?? humanPrefab.GetModuleFlags(),
|
||||
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
|
||||
element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
|
||||
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
|
||||
foreach (Identifier tag in humanPrefab.GetTags())
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
spawnedCharacter.GiveIdCardTags(wp);
|
||||
}
|
||||
|
||||
if (requiresRescue)
|
||||
{
|
||||
requireRescue.Add(spawnedCharacter);
|
||||
#if CLIENT
|
||||
if (allowOrderingRescuees)
|
||||
{
|
||||
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi)
|
||||
{
|
||||
var order = OrderPrefab.Prefabs["fightintruders"]
|
||||
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
|
||||
}
|
||||
|
||||
if (element.GetAttributeBool("requirekill", false))
|
||||
{
|
||||
requireKill.Add(spawnedCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
|
||||
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
|
||||
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
|
||||
characters.Add(spawnedCharacter);
|
||||
if (element.GetAttributeBool("requirekill", false))
|
||||
{
|
||||
requireKill.Add(spawnedCharacter);
|
||||
}
|
||||
if (spawnedCharacter.Inventory != null)
|
||||
{
|
||||
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
|
||||
}
|
||||
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(submarine);
|
||||
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
|
||||
foreach (Submarine sub in Submarine.MainSub.DockedTo)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (State != HostagesKilledState)
|
||||
@@ -316,7 +173,7 @@ namespace Barotrauma
|
||||
if (endTimer > EndDelay)
|
||||
{
|
||||
#if SERVER
|
||||
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
|
||||
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
|
||||
{
|
||||
GameMain.Server.EndGame();
|
||||
}
|
||||
@@ -337,7 +194,7 @@ namespace Barotrauma
|
||||
break;
|
||||
#if SERVER
|
||||
case 1:
|
||||
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
|
||||
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
|
||||
{
|
||||
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
|
||||
{
|
||||
@@ -360,5 +217,45 @@ namespace Barotrauma
|
||||
{
|
||||
failed = !completed && requireRescue.Any(r => r.Removed || r.IsDead);
|
||||
}
|
||||
|
||||
protected override void InitCharacter(Character character, XElement element)
|
||||
{
|
||||
base.InitCharacter(character, element);
|
||||
if (element.GetAttributeBool("requirekill", false))
|
||||
{
|
||||
requireKill.Add(character);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Character spawnedCharacter = base.LoadHuman(humanPrefab, element, submarine);
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
if (requiresRescue)
|
||||
{
|
||||
requireRescue.Add(spawnedCharacter);
|
||||
#if CLIENT
|
||||
if (allowOrderingRescuees)
|
||||
{
|
||||
GameMain.GameSession.CrewManager?.AddCharacterToCrewList(spawnedCharacter);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController)
|
||||
{
|
||||
var order = OrderPrefab.Prefabs["fightintruders"]
|
||||
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
|
||||
}
|
||||
// Overrides the team change set in the base method.
|
||||
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
|
||||
var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
|
||||
if (teamId != originalTeam)
|
||||
{
|
||||
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
|
||||
}
|
||||
return spawnedCharacter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,9 @@ namespace Barotrauma
|
||||
#if DEBUG || UNSTABLE
|
||||
if (State == 1 && !level.CheckBeaconActive())
|
||||
{
|
||||
DebugConsole.ThrowError("Beacon became inactive!");
|
||||
DebugConsole.ThrowError(
|
||||
"Debug/unstable only error message: beacon became inactive mid-mission after it had been activated! If this happened unexpectedly while you were away from the beacon, it may be a sign of a bug."+
|
||||
" If possible, please try to check what caused the beacon to go inactive.");
|
||||
State = 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace Barotrauma
|
||||
int maxCount = subElement.GetAttributeInt("maxcount", 10);
|
||||
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
|
||||
ItemPrefab itemPrefab = FindItemPrefab(subElement);
|
||||
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab))
|
||||
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanProbablyBePut(itemPrefab))
|
||||
{
|
||||
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
|
||||
itemsToSpawn.Add((subElement, containers[i].container));
|
||||
@@ -211,7 +211,7 @@ namespace Barotrauma
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
|
||||
public override int GetBaseReward(Submarine sub)
|
||||
public override float GetBaseReward(Submarine sub)
|
||||
{
|
||||
// If we are not at the location of the mission, skip the calculation of the reward
|
||||
if (GameMain.GameSession?.StartLocation != Locations[0])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -10,12 +11,59 @@ namespace Barotrauma
|
||||
private readonly LocalizedString[] descriptions;
|
||||
private static LocalizedString[] teamNames = { "Team A", "Team B" };
|
||||
|
||||
public override bool AllowRespawn
|
||||
private readonly bool allowRespawning;
|
||||
|
||||
enum WinCondition
|
||||
{
|
||||
get { return false; }
|
||||
/// <summary>
|
||||
/// The winner is the team with the last living player(s)
|
||||
/// </summary>
|
||||
LastManStanding,
|
||||
/// <summary>
|
||||
/// The team who reaches a specific number of kills (determined by WinScore) is the winner
|
||||
/// </summary>
|
||||
KillCount,
|
||||
/// <summary>
|
||||
/// The team who controls a specific submarine (can be a ruin, outpost or a beacon station too) for some time (determined by WinScore) is the winner
|
||||
/// </summary>
|
||||
ControlSubmarine
|
||||
}
|
||||
|
||||
private CharacterTeamType Winner
|
||||
private readonly WinCondition winCondition;
|
||||
|
||||
public override bool AllowRespawning
|
||||
{
|
||||
get => allowRespawning;
|
||||
}
|
||||
|
||||
private Submarine targetSubmarine;
|
||||
|
||||
private LocalizedString targetSubmarineSonarLabel;
|
||||
|
||||
/// <summary>
|
||||
/// Which type of submarine the team needs to stay in control of to win
|
||||
/// </summary>
|
||||
public TagAction.SubType TargetSubmarineType { get; set; }
|
||||
|
||||
public readonly int PointsPerKill;
|
||||
|
||||
/// <summary>
|
||||
/// The score required to win the mission.
|
||||
/// </summary>
|
||||
public int WinScore => GameMain.NetworkMember?.ServerSettings.WinScorePvP ?? 10;
|
||||
|
||||
/// <summary>
|
||||
/// Is the winner determined by some kind of a scoring mechanism?
|
||||
/// </summary>
|
||||
public bool HasWinScore =>
|
||||
winCondition != WinCondition.LastManStanding || PointsPerKill != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Scores of both teams. What the scoring represents depends on how the mission is configured (kills, time in control of a beacon station?)
|
||||
/// </summary>
|
||||
public readonly int[] Scores = new int[2];
|
||||
|
||||
public static CharacterTeamType Winner
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -46,6 +94,27 @@ namespace Barotrauma
|
||||
public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
allowRespawning = prefab.ConfigElement.GetAttributeBool(nameof(AllowRespawning), false);
|
||||
|
||||
winCondition = prefab.ConfigElement.GetAttributeEnum(nameof(WinCondition),
|
||||
allowRespawning ? WinCondition.KillCount : WinCondition.LastManStanding);
|
||||
|
||||
PointsPerKill = prefab.ConfigElement.GetAttributeInt(nameof(PointsPerKill), 0);
|
||||
|
||||
TargetSubmarineType = prefab.ConfigElement.GetAttributeEnum(nameof(TargetSubmarineType), TagAction.SubType.Any);
|
||||
|
||||
string sonarTag = prefab.ConfigElement.GetAttributeString(nameof(targetSubmarineSonarLabel), string.Empty);
|
||||
if (!sonarTag.IsNullOrEmpty())
|
||||
{
|
||||
targetSubmarineSonarLabel = TextManager.Get(sonarTag);
|
||||
}
|
||||
|
||||
if (allowRespawning && winCondition == WinCondition.LastManStanding)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in mission {prefab.Identifier}: win condition cannot be \"last man standing\" when respawning is enabled.",
|
||||
contentPackage: prefab.ContentPackage);
|
||||
}
|
||||
|
||||
descriptions = new LocalizedString[]
|
||||
{
|
||||
TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("descriptionneutral", "")),
|
||||
@@ -57,15 +126,23 @@ namespace Barotrauma
|
||||
{
|
||||
for (int n = 0; n < 2; n++)
|
||||
{
|
||||
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName);
|
||||
descriptions[i] =
|
||||
descriptions[i]
|
||||
.Replace($"[location{n + 1}]", locations[n].DisplayName)
|
||||
.Replace("[winscore]", WinScore.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
teamNames = new LocalizedString[]
|
||||
{
|
||||
TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname1", "Team A")),
|
||||
TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname2", "Team B"))
|
||||
TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname1", "missionteam1.pvpmission"))),
|
||||
TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname2", "missionteam2.pvpmission"))),
|
||||
};
|
||||
|
||||
if (winCondition == WinCondition.KillCount && PointsPerKill == 0)
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in mission {Prefab.Identifier}: win condition is kill count, but {nameof(PointsPerKill)} is set to 0.");
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalizedString GetTeamName(CharacterTeamType teamID)
|
||||
@@ -82,9 +159,9 @@ namespace Barotrauma
|
||||
return "Invalid Team";
|
||||
}
|
||||
|
||||
public bool IsInWinningTeam(Character character)
|
||||
public static bool IsInWinningTeam(Character character)
|
||||
{
|
||||
return character != null &&
|
||||
return character != null &&
|
||||
Winner != CharacterTeamType.None &&
|
||||
Winner == character.TeamID;
|
||||
}
|
||||
@@ -99,19 +176,32 @@ namespace Barotrauma
|
||||
|
||||
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
|
||||
|
||||
subs[0].NeutralizeBallast();
|
||||
subs[0].TeamID = CharacterTeamType.Team1;
|
||||
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
|
||||
if (Prefab.LoadSubmarines)
|
||||
{
|
||||
subs[0].NeutralizeBallast();
|
||||
subs[0].TeamID = CharacterTeamType.Team1;
|
||||
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
|
||||
|
||||
subs[1].NeutralizeBallast();
|
||||
subs[1].TeamID = CharacterTeamType.Team2;
|
||||
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
|
||||
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
|
||||
subs[1].FlipX();
|
||||
subs[1].NeutralizeBallast();
|
||||
subs[1].TeamID = CharacterTeamType.Team2;
|
||||
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
|
||||
GameSession.PlaceSubAtInitialPosition(subs[1], level, placeAtStart: false);
|
||||
subs[1].FlipX();
|
||||
}
|
||||
#if SERVER
|
||||
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
|
||||
roundEndTimer = RoundEndDuration;
|
||||
#endif
|
||||
|
||||
if (TargetSubmarineType != TagAction.SubType.Any)
|
||||
{
|
||||
targetSubmarine = Submarine.Loaded.FirstOrDefault(s => TagAction.SubmarineTypeMatches(s, TargetSubmarineType));
|
||||
if (targetSubmarine == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in mission {Prefab.Identifier}: could not find a submarine of the type {TargetSubmarineType}.",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted()
|
||||
|
||||
@@ -4,17 +4,13 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EscortMission : Mission
|
||||
{
|
||||
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;
|
||||
@@ -36,7 +32,6 @@ namespace Barotrauma
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
missionSub = sub;
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
|
||||
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
|
||||
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
|
||||
@@ -61,7 +56,7 @@ namespace Barotrauma
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
|
||||
public override int GetBaseReward(Submarine sub)
|
||||
public override float GetBaseReward(Submarine sub)
|
||||
{
|
||||
if (sub != missionSub)
|
||||
{
|
||||
@@ -192,7 +187,6 @@ namespace Barotrauma
|
||||
foreach (ContentXElement element in characterConfig.Elements())
|
||||
{
|
||||
string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
|
||||
string colorIdentifier = element.GetAttributeString("color", string.Empty);
|
||||
for (int k = 0; k < scalingCharacterCount; k++)
|
||||
{
|
||||
// for each element defined, we need to initialize that type of character equal to the scaling escorted character count
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Barotrauma
|
||||
get { return failed; }
|
||||
}
|
||||
|
||||
public virtual bool AllowRespawn
|
||||
public virtual bool AllowRespawning
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
@@ -161,6 +161,10 @@ namespace Barotrauma
|
||||
private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
|
||||
|
||||
public Action<Mission> OnMissionStateChanged;
|
||||
|
||||
protected readonly ContentXElement characterConfig;
|
||||
protected readonly List<Character> characters = new List<Character>();
|
||||
protected readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
|
||||
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
{
|
||||
@@ -192,6 +196,8 @@ namespace Barotrauma
|
||||
messages[m] = ReplaceVariablesInMissionMessage(messages[m], sub);
|
||||
}
|
||||
Messages = messages.ToImmutableArray();
|
||||
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
}
|
||||
|
||||
public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true)
|
||||
@@ -211,21 +217,21 @@ namespace Barotrauma
|
||||
|
||||
public virtual void SetLevel(LevelData level) { }
|
||||
|
||||
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
|
||||
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
|
||||
{
|
||||
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer, difficultyLevel);
|
||||
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionTypes, isSinglePlayer, difficultyLevel);
|
||||
}
|
||||
|
||||
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
|
||||
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
|
||||
{
|
||||
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
|
||||
if (missionType == MissionType.None)
|
||||
if (missionTypes.None())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => m.Type.HasAnyFlag(missionType)));
|
||||
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => missionTypes.Contains(m.Type)));
|
||||
}
|
||||
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
|
||||
if (requireCorrectLocationType)
|
||||
@@ -244,25 +250,179 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Calculates the base reward, can be overridden for different mission types
|
||||
/// </summary>
|
||||
public virtual int GetBaseReward(Submarine sub)
|
||||
public virtual float GetBaseReward(Submarine sub)
|
||||
{
|
||||
return Prefab.Reward;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the available reward, taking into account universal modifiers such as campaign settings
|
||||
/// Calculates the available monetary reward, taking into account universal modifiers such as campaign settings.
|
||||
/// </summary>
|
||||
public int GetReward(Submarine sub)
|
||||
{
|
||||
int reward = GetBaseReward(sub);
|
||||
|
||||
float reward = GetBaseReward(sub);
|
||||
// Some modifiers should apply universally to all implementations of GetBaseReward
|
||||
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
|
||||
{
|
||||
reward = (int)Math.Round(reward * campaign.Settings.MissionRewardMultiplier);
|
||||
reward *= campaign.Settings.MissionRewardMultiplier;
|
||||
}
|
||||
return (int)Math.Round(reward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call to load character elements to be spawned. Has to be implemented (and synced) separately per each mission.
|
||||
/// </summary>
|
||||
protected void InitCharacters(Submarine submarine)
|
||||
{
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
|
||||
return reward;
|
||||
if (characterConfig != null)
|
||||
{
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
|
||||
|
||||
int defaultCount = element.GetAttributeInt("count", -1);
|
||||
if (defaultCount < 0)
|
||||
{
|
||||
defaultCount = element.GetAttributeInt("amount", 1);
|
||||
}
|
||||
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
|
||||
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
|
||||
int count = Rand.Range(min, max + 1);
|
||||
|
||||
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
|
||||
{
|
||||
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
|
||||
if (humanPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a human character for a mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadHuman(humanPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty));
|
||||
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (characterPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a character for a mission: character prefab \"{speciesName}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadMonster(characterPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SpawnAction.SpawnLocationType GetSpawnLocationTypeFromSubmarineType(Submarine sub)
|
||||
{
|
||||
return sub.Info.Type switch
|
||||
{
|
||||
SubmarineType.Outpost or SubmarineType.OutpostModule => SpawnAction.SpawnLocationType.Outpost,
|
||||
SubmarineType.Wreck => SpawnAction.SpawnLocationType.Wreck,
|
||||
SubmarineType.Ruin => SpawnAction.SpawnLocationType.Ruin,
|
||||
SubmarineType.BeaconStation => SpawnAction.SpawnLocationType.BeaconStation,
|
||||
SubmarineType.Player => SpawnAction.SpawnLocationType.MainSub,
|
||||
_ => SpawnAction.SpawnLocationType.Any
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
|
||||
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
|
||||
var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
|
||||
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
|
||||
GetSpawnLocationTypeFromSubmarineType(submarine), spawnPointType,
|
||||
moduleFlags ?? humanPrefab.GetModuleFlags(),
|
||||
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
|
||||
element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
var teamId = element.GetAttributeEnum("teamid", CharacterTeamType.None);
|
||||
var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, originalTeam, spawnPos);
|
||||
//consider the NPC to be "originally" from the team of the outpost it spawns in, and change it to the desired (hostile) team afterwards
|
||||
//that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost
|
||||
if (teamId != originalTeam)
|
||||
{
|
||||
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
|
||||
}
|
||||
if (element.GetAttribute("color") != null)
|
||||
{
|
||||
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
|
||||
}
|
||||
if (submarine.Info is { IsOutpost: true } outPostInfo)
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
|
||||
foreach (Identifier tag in humanPrefab.GetTags())
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
|
||||
}
|
||||
}
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
spawnedCharacter.GiveIdCardTags(wp);
|
||||
}
|
||||
InitCharacter(spawnedCharacter, element);
|
||||
return spawnedCharacter;
|
||||
}
|
||||
|
||||
protected virtual Character LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
|
||||
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
|
||||
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
|
||||
characters.Add(spawnedCharacter);
|
||||
if (spawnedCharacter.Inventory != null)
|
||||
{
|
||||
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
|
||||
}
|
||||
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(submarine);
|
||||
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
|
||||
foreach (Submarine sub in Submarine.MainSub.DockedTo)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(sub);
|
||||
}
|
||||
}
|
||||
InitCharacter(spawnedCharacter, element);
|
||||
return spawnedCharacter;
|
||||
}
|
||||
|
||||
protected virtual void InitCharacter(Character character, XElement element)
|
||||
{
|
||||
if (element.GetAttributeBool(Tags.IgnoredByAI.Value, false))
|
||||
{
|
||||
character.AddAbilityFlag(AbilityFlags.IgnoredByEnemyAI);
|
||||
}
|
||||
float playDeadProbability = element.GetAttributeFloat("playdeadprobability", -1);
|
||||
if (playDeadProbability >= 0)
|
||||
{
|
||||
character.EvaluatePlayDeadProbability(playDeadProbability);
|
||||
}
|
||||
float huskProbability = element.GetAttributeFloat("huskprobability", 0);
|
||||
if (huskProbability > 0 && Rand.Value() <= huskProbability)
|
||||
{
|
||||
character.TurnIntoHusk();
|
||||
}
|
||||
else if (element.GetAttributeBool("corpse", false))
|
||||
{
|
||||
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Start(Level level)
|
||||
@@ -350,11 +510,12 @@ namespace Barotrauma
|
||||
private void TriggerEvent(MissionPrefab.TriggerEvent trigger)
|
||||
{
|
||||
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
|
||||
var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier);
|
||||
//clients are not allowed to trigger events, they're handled by the server
|
||||
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(trigger.EventIdentifier, trigger.EventTag, Prefab.ContentPackage);
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
DebugConsole.ThrowError($"Mission {Prefab.Identifier} failed to trigger an event (identifier: {trigger.EventIdentifier}, tag: {trigger.EventTag}).", contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
if (GameMain.GameSession?.EventManager != null)
|
||||
@@ -427,15 +588,23 @@ namespace Barotrauma
|
||||
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
|
||||
}
|
||||
|
||||
private float CalculateDifficultyXPMultiplier()
|
||||
{
|
||||
const float minMissionDifficulty = 1;
|
||||
const float maxMissionDifficulty = 4;
|
||||
const float maxXpBonus = 1.3f;
|
||||
float selectedMissionDifficulty = MathUtils.InverseLerp(minMissionDifficulty, maxMissionDifficulty, Prefab.Difficulty.GetValueOrDefault());
|
||||
float xpBonusMultiplier = MathHelper.Lerp(1.0f, maxXpBonus, selectedMissionDifficulty);
|
||||
|
||||
return xpBonusMultiplier;
|
||||
}
|
||||
|
||||
private void GiveReward()
|
||||
{
|
||||
if (GameMain.GameSession.GameMode is not CampaignMode campaign) { return; }
|
||||
int reward = GetReward(Submarine.MainSub);
|
||||
|
||||
float baseExperienceGain = reward * 0.09f;
|
||||
|
||||
float difficultyMultiplier = 1 + level.Difficulty / 100f;
|
||||
baseExperienceGain *= difficultyMultiplier;
|
||||
float xpReward = GetBaseReward(Submarine.MainSub) * Prefab.ExperienceMultiplier * campaign.Settings.ExperienceRewardMultiplier;
|
||||
float xpGain = xpReward * level.LevelData.Biome.ExperienceFromMissionRewards * CalculateDifficultyXPMultiplier();
|
||||
|
||||
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
@@ -443,7 +612,7 @@ namespace Barotrauma
|
||||
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null);
|
||||
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
|
||||
|
||||
DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value));
|
||||
DistributeExperienceToCrew(crewCharacters, (int)(xpGain * experienceGainMultiplier.Value));
|
||||
|
||||
CalculateFinalReward(Submarine.MainSub);
|
||||
#if SERVER
|
||||
@@ -567,7 +736,7 @@ namespace Barotrauma
|
||||
|
||||
Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty);
|
||||
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
|
||||
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier, contentPackageToLogInError: Prefab.ContentPackage);
|
||||
if (humanPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
@@ -7,52 +8,42 @@ using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
[Flags]
|
||||
public enum MissionType
|
||||
{
|
||||
None = 0x0,
|
||||
Salvage = 0x1,
|
||||
Monster = 0x2,
|
||||
Cargo = 0x4,
|
||||
Beacon = 0x8,
|
||||
Nest = 0x10,
|
||||
Mineral = 0x20,
|
||||
Combat = 0x40,
|
||||
AbandonedOutpost = 0x80,
|
||||
Escort = 0x100,
|
||||
Pirate = 0x200,
|
||||
GoTo = 0x400,
|
||||
ScanAlienRuins = 0x800,
|
||||
EliminateTargets = 0x1000,
|
||||
End = 0x2000,
|
||||
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | EliminateTargets | End
|
||||
}
|
||||
|
||||
partial class MissionPrefab : PrefabWithUintIdentifier
|
||||
{
|
||||
public static readonly PrefabCollection<MissionPrefab> Prefabs = new PrefabCollection<MissionPrefab>();
|
||||
|
||||
public static readonly Dictionary<MissionType, Type> CoOpMissionClasses = new Dictionary<MissionType, Type>()
|
||||
/// <summary>
|
||||
/// The keys here are for backwards compatibility, tying the old mission types to the appropriate class.
|
||||
/// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string.
|
||||
/// </summary>
|
||||
public static readonly Dictionary<Identifier, Type> CoOpMissionClasses = new Dictionary<Identifier, Type>()
|
||||
{
|
||||
{ MissionType.Salvage, typeof(SalvageMission) },
|
||||
{ MissionType.Monster, typeof(MonsterMission) },
|
||||
{ MissionType.Cargo, typeof(CargoMission) },
|
||||
{ MissionType.Beacon, typeof(BeaconMission) },
|
||||
{ MissionType.Nest, typeof(NestMission) },
|
||||
{ MissionType.Mineral, typeof(MineralMission) },
|
||||
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
|
||||
{ MissionType.Escort, typeof(EscortMission) },
|
||||
{ MissionType.Pirate, typeof(PirateMission) },
|
||||
{ MissionType.GoTo, typeof(GoToMission) },
|
||||
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
|
||||
{ MissionType.EliminateTargets, typeof(EliminateTargetsMission) },
|
||||
{ MissionType.End, typeof(EndMission) }
|
||||
{ "Salvage".ToIdentifier(), typeof(SalvageMission) },
|
||||
{ "Monster".ToIdentifier(), typeof(MonsterMission) },
|
||||
{ "Cargo".ToIdentifier(), typeof(CargoMission) },
|
||||
{ "Beacon".ToIdentifier(), typeof(BeaconMission) },
|
||||
{ "Nest".ToIdentifier(), typeof(NestMission) },
|
||||
{ "Mineral".ToIdentifier(), typeof(MineralMission) },
|
||||
{ "AbandonedOutpost".ToIdentifier(), typeof(AbandonedOutpostMission) },
|
||||
{ "Escort".ToIdentifier(), typeof(EscortMission) },
|
||||
{ "Pirate".ToIdentifier(), typeof(PirateMission) },
|
||||
{ "GoTo".ToIdentifier(), typeof(GoToMission) },
|
||||
{ "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) },
|
||||
{ "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) },
|
||||
{ "End".ToIdentifier(), typeof(EndMission) }
|
||||
};
|
||||
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
|
||||
|
||||
/// <summary>
|
||||
/// The keys here are for backwards compatibility, tying the old mission types to the appropriate class.
|
||||
/// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string.
|
||||
/// </summary>
|
||||
public static readonly Dictionary<Identifier, Type> PvPMissionClasses = new Dictionary<Identifier, Type>()
|
||||
{
|
||||
{ MissionType.Combat, typeof(CombatMission) }
|
||||
{ "Combat".ToIdentifier(), typeof(CombatMission) }
|
||||
};
|
||||
|
||||
public static readonly HashSet<Identifier> HiddenMissionTypes = new HashSet<Identifier>() { "GoTo".ToIdentifier(), "End".ToIdentifier() };
|
||||
|
||||
public class ReputationReward
|
||||
{
|
||||
public readonly Identifier FactionIdentifier;
|
||||
@@ -67,11 +58,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo, MissionType.End };
|
||||
|
||||
private readonly ConstructorInfo constructor;
|
||||
|
||||
public readonly MissionType Type;
|
||||
public readonly Identifier Type;
|
||||
|
||||
public readonly Type MissionClass;
|
||||
|
||||
public readonly bool MultiplayerOnly, SingleplayerOnly;
|
||||
|
||||
@@ -110,6 +101,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly int Reward;
|
||||
|
||||
public readonly float ExperienceMultiplier;
|
||||
|
||||
// The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters.
|
||||
public readonly ImmutableArray<LocalizedString> Headers;
|
||||
public readonly ImmutableArray<LocalizedString> Messages;
|
||||
@@ -122,7 +115,21 @@ namespace Barotrauma
|
||||
|
||||
public readonly bool AllowOtherMissionsInLevel;
|
||||
|
||||
public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck;
|
||||
public readonly bool RequireWreck, RequireRuin, RequireBeaconStation, RequireThalamusWreck;
|
||||
public readonly bool SpawnBeaconStationInMiddle;
|
||||
|
||||
public readonly bool AllowOutpostNPCs;
|
||||
|
||||
public readonly Identifier ForceOutpostGenerationParameters;
|
||||
|
||||
public readonly RespawnMode? ForceRespawnMode;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the players can choose which outpost is used for the mission (selected from the outposts that have this tag). Only works in multiplayer.
|
||||
/// </summary>
|
||||
public readonly Identifier AllowOutpostSelectionFromTag;
|
||||
|
||||
public readonly bool LoadSubmarines = true;
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, locations this mission takes place in cannot change their type
|
||||
@@ -157,7 +164,10 @@ namespace Barotrauma
|
||||
public class TriggerEvent
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public string EventIdentifier { get; private set; }
|
||||
public Identifier EventIdentifier { get; private set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier EventTag { get; private set; }
|
||||
|
||||
[Serialize(0, IsPropertySaveable.Yes)]
|
||||
public int State { get; private set; }
|
||||
@@ -210,16 +220,21 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Reward = element.GetAttributeInt("reward", 1);
|
||||
ExperienceMultiplier = element.GetAttributeFloat("experiencemultiplier", 1.0f);
|
||||
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);
|
||||
RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false);
|
||||
|
||||
RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false);
|
||||
RequireThalamusWreck = element.GetAttributeBool(nameof(RequireThalamusWreck), false);
|
||||
RequireRuin = element.GetAttributeBool(nameof(RequireRuin), false);
|
||||
RequireBeaconStation = element.GetAttributeBool(nameof(RequireBeaconStation), false);
|
||||
SpawnBeaconStationInMiddle = element.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false);
|
||||
if (RequireThalamusWreck) { RequireWreck = true; }
|
||||
|
||||
LoadSubmarines = element.GetAttributeBool(nameof(LoadSubmarines), true);
|
||||
|
||||
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
|
||||
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
|
||||
Commonness = element.GetAttributeInt("commonness", 1);
|
||||
@@ -234,6 +249,15 @@ namespace Barotrauma
|
||||
MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100));
|
||||
MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100);
|
||||
|
||||
AllowOutpostNPCs = element.GetAttributeBool(nameof(AllowOutpostNPCs), true);
|
||||
ForceOutpostGenerationParameters = element.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty);
|
||||
AllowOutpostSelectionFromTag = element.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty);
|
||||
|
||||
if (element.GetAttribute(nameof(ForceRespawnMode)) != null)
|
||||
{
|
||||
ForceRespawnMode = element.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound);
|
||||
}
|
||||
|
||||
ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false);
|
||||
ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false);
|
||||
MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1);
|
||||
@@ -362,47 +386,23 @@ namespace Barotrauma
|
||||
Messages = messages.ToImmutableArray();
|
||||
ReputationRewards = reputationRewards.ToImmutableList();
|
||||
|
||||
Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty);
|
||||
//backwards compatibility
|
||||
|
||||
if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
|
||||
{
|
||||
missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier();
|
||||
}
|
||||
else if (missionTypeName == "clearalienruins")
|
||||
{
|
||||
missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier();
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(missionTypeName.Value, true, out Type))
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
|
||||
return;
|
||||
}
|
||||
if (Type == MissionType.None)
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
|
||||
return;
|
||||
}
|
||||
MissionClass = FindMissionClass(element);
|
||||
Type = element.GetAttributeIdentifier(nameof(Type), Identifier.Empty);
|
||||
|
||||
#if DEBUG
|
||||
if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty())
|
||||
if (MissionClass == typeof(MonsterMission) && SonarLabel.IsNullOrEmpty())
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set.");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (CoOpMissionClasses.ContainsKey(Type))
|
||||
if (!LoadSubmarines && MissionClass != typeof(CombatMission))
|
||||
{
|
||||
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
|
||||
}
|
||||
else if (PvPMissionClasses.ContainsKey(Type))
|
||||
{
|
||||
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\"");
|
||||
DebugConsole.AddWarning($"Potential error in mission {Identifier}: Disabling submarines is only intended for combat missions taking place in an outpost, and may lead to issues in other types of missions.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
|
||||
constructor = FindMissionConstructor(element, MissionClass);
|
||||
if (constructor == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!",
|
||||
@@ -411,6 +411,67 @@ namespace Barotrauma
|
||||
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
|
||||
private Type FindMissionClass(ContentXElement element)
|
||||
{
|
||||
Type type;
|
||||
Identifier typeName = element.NameAsIdentifier();
|
||||
type = TryGetClass(typeName.RemoveFromEnd("Mission"));
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
//backwards compatibility: the actual mission class used to be defined by the "type" attribute,
|
||||
//Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string,
|
||||
//but if we failed to find the class based on the name, let's try the type attribute.
|
||||
Identifier typeNameLegacy = (element.GetAttributeIdentifier("type", Identifier.Empty)).ToIdentifier();
|
||||
if (typeNameLegacy == "OutpostDestroy" || typeNameLegacy == "OutpostRescue")
|
||||
{
|
||||
typeNameLegacy = "AbandonedOutpost".ToIdentifier();
|
||||
}
|
||||
else if (typeNameLegacy == "clearalienruins")
|
||||
{
|
||||
typeNameLegacy = "EliminateTargets".ToIdentifier();
|
||||
}
|
||||
type = TryGetClass(typeNameLegacy) ?? TryGetClass(typeNameLegacy.AppendIfMissing("Mission"));
|
||||
if (type == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find the mission type \"{typeNameLegacy}\" for the mission {Identifier}.",
|
||||
contentPackage: element.ContentPackage);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Type TryGetClass(Identifier typeName)
|
||||
{
|
||||
if (CoOpMissionClasses.TryGetValue(typeName, out Type coOpMissionClass))
|
||||
{
|
||||
return coOpMissionClass;
|
||||
}
|
||||
else if (PvPMissionClasses.TryGetValue(typeName, out Type pvpMissionClass))
|
||||
{
|
||||
return pvpMissionClass;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private ConstructorInfo FindMissionConstructor(ContentXElement element, Type missionClass)
|
||||
{
|
||||
ConstructorInfo constructor;
|
||||
if (missionClass == null) { return null; }
|
||||
if (missionClass != typeof(Mission) && !missionClass.IsSubclassOf(typeof(Mission))) { return null; }
|
||||
constructor = missionClass.GetConstructor(new Type[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
|
||||
if (constructor == null)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Could not find the constructor of the mission type \"{missionClass}\" for the mission {Identifier}",
|
||||
contentPackage: element.ContentPackage);
|
||||
return null;
|
||||
}
|
||||
return constructor;
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
|
||||
@@ -424,7 +485,7 @@ namespace Barotrauma
|
||||
}
|
||||
return
|
||||
AllowedLocationTypes.Any(lt => lt == "any") ||
|
||||
AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) ||
|
||||
AllowedLocationTypes.Any(lt => lt == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost) ||
|
||||
AllowedLocationTypes.Any(lt => lt == from.Type.Identifier);
|
||||
}
|
||||
|
||||
@@ -432,11 +493,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (fromType == "any" ||
|
||||
fromType == from.Type.Identifier ||
|
||||
(fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned"))
|
||||
(fromType == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost && from.Type.Identifier != "abandoned"))
|
||||
{
|
||||
if (toType == "any" ||
|
||||
toType == to.Type.Identifier ||
|
||||
(toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned"))
|
||||
(toType == Barotrauma.Tags.AnyOutpost && to.HasOutpost() && to.Type.IsAnyOutpost && to.Type.Identifier != "abandoned"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -461,5 +522,28 @@ namespace Barotrauma
|
||||
{
|
||||
DisposeProjectSpecific();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission
|
||||
/// (the mission at the end of the campaign)
|
||||
/// </summary>
|
||||
public static IEnumerable<Identifier> GetAllMultiplayerSelectableMissionTypes()
|
||||
{
|
||||
List<Identifier> missionTypes = new List<Identifier>();
|
||||
foreach (var missionPrefab in Prefabs)
|
||||
{
|
||||
if (missionPrefab.Commonness <= 0.0f) { continue; }
|
||||
if (missionPrefab.SingleplayerOnly) { continue; }
|
||||
if (HiddenMissionTypes.Contains(missionPrefab.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!missionTypes.Contains(missionPrefab.Type))
|
||||
{
|
||||
missionTypes.Add(missionPrefab.Type);
|
||||
}
|
||||
}
|
||||
return missionTypes.OrderBy(t => t.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace Barotrauma
|
||||
partial class PirateMission : Mission
|
||||
{
|
||||
private readonly ContentXElement submarineTypeConfig;
|
||||
private readonly ContentXElement characterConfig;
|
||||
private readonly ContentXElement characterTypeConfig;
|
||||
private readonly float addedMissionDifficultyPerPlayer;
|
||||
|
||||
@@ -22,8 +21,8 @@ namespace Barotrauma
|
||||
private Identifier factionIdentifier;
|
||||
|
||||
private Submarine enemySub;
|
||||
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>>();
|
||||
|
||||
// Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it.
|
||||
private readonly float pirateSightingUpdateFrequency = 30;
|
||||
@@ -68,7 +67,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetBaseReward(Submarine sub)
|
||||
public override float GetBaseReward(Submarine sub)
|
||||
{
|
||||
return alternateReward;
|
||||
}
|
||||
@@ -92,18 +91,29 @@ namespace Barotrauma
|
||||
public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
submarineTypeConfig = prefab.ConfigElement.GetChildElement("SubmarineTypes");
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes");
|
||||
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
|
||||
|
||||
factionIdentifier = prefab.ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
|
||||
//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);
|
||||
Identifier typeId = characterElement.GetAttributeIdentifier("typeidentifier", Identifier.Empty);
|
||||
if (typeId.IsEmpty)
|
||||
{
|
||||
if (characterElement.GetAttributeIdentifier("identifier", Identifier.Empty).IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character element with neither a typeidentifier or identifier ({characterElement.ToString()}).",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e =>
|
||||
e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == typeId);
|
||||
if (characterTypeElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".",
|
||||
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{typeId}\".",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +124,7 @@ namespace Barotrauma
|
||||
{
|
||||
Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty);
|
||||
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
|
||||
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier, contentPackageToLogInError: Prefab.ContentPackage);
|
||||
if (humanPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".",
|
||||
@@ -143,37 +153,47 @@ namespace Barotrauma
|
||||
levelData = level;
|
||||
missionDifficulty = level?.Difficulty ?? 0;
|
||||
|
||||
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
|
||||
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
|
||||
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
//no specific sub configured, choose a random one
|
||||
if (submarineTypeConfig == null)
|
||||
{
|
||||
submarineInfo = GetRandomDifficultyModifiedSubmarine(missionDifficulty, ShipRandomnessModifier);
|
||||
alternateReward = (int)submarineInfo.EnemySubmarineInfo.Reward;
|
||||
}
|
||||
else
|
||||
{
|
||||
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
|
||||
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
|
||||
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", factionIdentifier);
|
||||
|
||||
ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
|
||||
if (submarinePath.IsNullOrEmpty())
|
||||
{
|
||||
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
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}!",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
|
||||
}
|
||||
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
|
||||
ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
|
||||
if (submarinePath.IsNullOrEmpty())
|
||||
{
|
||||
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
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}!",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
return;
|
||||
}
|
||||
|
||||
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
|
||||
}
|
||||
|
||||
private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
|
||||
@@ -185,6 +205,33 @@ namespace Barotrauma
|
||||
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
|
||||
}
|
||||
|
||||
private SubmarineInfo GetRandomDifficultyModifiedSubmarine(float levelDifficulty, float randomnessModifier)
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
// look for the saved submarine that is closest to our difficulty, with some randomness
|
||||
SubmarineInfo bestSubmarine = null;
|
||||
float bestValue = float.MaxValue;
|
||||
var submarineInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsEnemySubmarine);
|
||||
foreach (SubmarineInfo submarineInfo in submarineInfos)
|
||||
{
|
||||
if (!Prefab.Tags.Any(t => submarineInfo.EnemySubmarineInfo.MissionTags.Contains(t))) { continue; }
|
||||
float applicabilityValue = GetDifficultyModifiedValue(submarineInfo.EnemySubmarineInfo.PreferredDifficulty, levelDifficulty, randomnessModifier, rand);
|
||||
if (applicabilityValue < bestValue)
|
||||
{
|
||||
bestSubmarine = submarineInfo;
|
||||
bestValue = applicabilityValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestSubmarine == null)
|
||||
{
|
||||
DebugConsole.ThrowError("No EnemySubmarine found that matches the mission's tags!");
|
||||
return SubmarineInfo.SavedSubmarines.First(i => i.IsEnemySubmarine);
|
||||
}
|
||||
|
||||
return bestSubmarine;
|
||||
}
|
||||
|
||||
private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
@@ -300,30 +347,54 @@ namespace Barotrauma
|
||||
bool commanderAssigned = false;
|
||||
foreach (ContentXElement element in characterConfig.Elements())
|
||||
{
|
||||
//there's two ways to define the characters in pirate missions
|
||||
//1. "the normal way", referring to a human prefab
|
||||
Identifier humanPrefabId = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
//2. the strange way it was initially implemented and the way the vanilla missions work: using a reference to a "character type" in the mission, which refers to a human prefab
|
||||
Identifier characterTypeId = element.GetAttributeIdentifier("typeidentifier", Identifier.Empty);
|
||||
|
||||
int minAmount = element.GetAttributeInt("minamount", 0);
|
||||
int maxAmount = element.GetAttributeInt("maxamount", 0);
|
||||
// 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);
|
||||
int amountCreated = minAmount == 0 && maxAmount == 0 ?
|
||||
//default to 1 character if amount is not defined
|
||||
1 :
|
||||
//otherwise choose a value between min and max based on difficulty
|
||||
GetDifficultyModifiedAmount(minAmount, maxAmount, enemyCreationDifficulty, rand);
|
||||
|
||||
for (int i = 0; i < amountCreated; i++)
|
||||
{
|
||||
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault();
|
||||
|
||||
if (characterType == null)
|
||||
HumanPrefab humanPrefab = null;
|
||||
bool isCommander = false;
|
||||
if (!characterTypeId.IsEmpty)
|
||||
{
|
||||
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".",
|
||||
contentPackage: element.ContentPackage);
|
||||
return;
|
||||
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == characterTypeId).FirstOrDefault();
|
||||
if (characterType == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".",
|
||||
contentPackage: element.ContentPackage);
|
||||
return;
|
||||
}
|
||||
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
|
||||
humanPrefab = GetHumanPrefabFromElement(variantElement);
|
||||
isCommander = variantElement.GetAttributeBool("iscommander", false);
|
||||
}
|
||||
else if (!humanPrefabId.IsEmpty)
|
||||
{
|
||||
humanPrefab = GetHumanPrefabFromElement(element);
|
||||
isCommander = element.GetAttributeBool("iscommander", false);
|
||||
}
|
||||
|
||||
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
|
||||
|
||||
var humanPrefab = GetHumanPrefabFromElement(variantElement);
|
||||
if (humanPrefab == null) { continue; }
|
||||
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null);
|
||||
if (element.GetAttribute("color") != null)
|
||||
{
|
||||
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
|
||||
}
|
||||
if (!commanderAssigned)
|
||||
{
|
||||
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
|
||||
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
|
||||
{
|
||||
humanAIController.InitShipCommandManager();
|
||||
@@ -335,6 +406,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (subElement.NameAsIdentifier() == "statuseffect")
|
||||
{
|
||||
var newEffect = StatusEffect.Load(subElement, parentDebugName: Prefab.Name.Value);
|
||||
newEffect?.Apply(newEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Item item in spawnedCharacter.Inventory.AllItems)
|
||||
{
|
||||
if (item?.GetComponent<IdCard>() != null)
|
||||
@@ -395,10 +475,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
enemySub.SetPosition(spawnPos);
|
||||
if (!IsClient)
|
||||
{
|
||||
InitPirateShip();
|
||||
}
|
||||
InitPirateShip();
|
||||
|
||||
// flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine
|
||||
// creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing
|
||||
|
||||
@@ -39,6 +39,12 @@ namespace Barotrauma
|
||||
public readonly Identifier ContainerTag;
|
||||
public readonly Identifier ExistingItemTag;
|
||||
|
||||
/// <summary>
|
||||
/// If true, target location indicator points to the submarine where the target is inside when the target is not yet found. Not used, if target is not inside any submarine.
|
||||
/// When enabled, the indicator is hidden when the player is inside the target submarine.
|
||||
/// </summary>
|
||||
public readonly bool PointToSub;
|
||||
|
||||
public readonly bool RemoveItem;
|
||||
|
||||
public readonly LocalizedString SonarLabel;
|
||||
@@ -55,6 +61,8 @@ namespace Barotrauma
|
||||
public readonly RetrievalState RequiredRetrievalState;
|
||||
|
||||
public readonly bool HideLabelAfterRetrieved;
|
||||
public readonly bool HideLabelWhenFound;
|
||||
public readonly bool HideLabelWhenNotFound;
|
||||
|
||||
public bool Retrieved
|
||||
{
|
||||
@@ -115,6 +123,9 @@ namespace Barotrauma
|
||||
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", parentTarget?.RequiredRetrievalState ?? RetrievalState.RetrievedToSub);
|
||||
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", parentTarget != null);
|
||||
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", parentTarget?.HideLabelAfterRetrieved ?? false);
|
||||
HideLabelWhenFound = element.GetAttributeBool(nameof(HideLabelWhenFound), parentTarget?.HideLabelWhenFound ?? false);
|
||||
HideLabelWhenNotFound = element.GetAttributeBool(nameof(HideLabelWhenNotFound), parentTarget?.HideLabelWhenNotFound ?? false);
|
||||
PointToSub = element.GetAttributeBool(nameof(PointToSub), parentTarget?.PointToSub ?? false);
|
||||
RequireInsideOriginalContainer = element.GetAttributeBool("requireinsideoriginalcontainer", false);
|
||||
|
||||
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
|
||||
@@ -203,6 +214,8 @@ namespace Barotrauma
|
||||
/// What percentage of targets need to be retrieved for the mission to complete (0.0 - 1.0). Defaults to 0.98.
|
||||
/// </summary>
|
||||
private readonly float requiredDeliveryAmount;
|
||||
|
||||
private LocalizedString pickedUpMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Message displayed when at least one of the targets is retrieved, but the mission is not complete yet.
|
||||
@@ -225,8 +238,26 @@ namespace Barotrauma
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; }
|
||||
if (target.Item != null && !target.Item.Removed)
|
||||
if (target.State is Target.RetrievalState.None)
|
||||
{
|
||||
if (target.HideLabelWhenNotFound) { continue; }
|
||||
}
|
||||
else if (target.HideLabelWhenFound)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (target.Item is { Removed: false })
|
||||
{
|
||||
if (target.PointToSub && target.Item.Submarine is Submarine targetSub && target.State == Target.RetrievalState.None)
|
||||
{
|
||||
if (Character.Controlled is Character playerCharacter && playerCharacter.Submarine != targetSub)
|
||||
{
|
||||
// The target is not in the same sub as the player -> point to the target submarine (instead of the item position).
|
||||
// When inside the target sub, don't show anything.
|
||||
yield return (target.SonarLabel, targetSub.WorldPosition);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (target.Item.ParentInventory?.Owner is Item parentItem)
|
||||
{
|
||||
bool insideParentItem = false;
|
||||
@@ -238,7 +269,7 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
//if the item is inside another target that has it's own sonar label, no need to show one on this item
|
||||
//if the item is inside another target that has its own sonar label, no need to show one on this item
|
||||
if (insideParentItem) { continue; }
|
||||
}
|
||||
|
||||
@@ -263,6 +294,7 @@ namespace Barotrauma
|
||||
|
||||
partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage));
|
||||
allRetrievedMessage = GetMessage(nameof(allRetrievedMessage));
|
||||
pickedUpMessage = GetMessage(nameof(pickedUpMessage));
|
||||
|
||||
foreach (ContentXElement subElement in prefab.ConfigElement.Elements())
|
||||
{
|
||||
@@ -311,7 +343,11 @@ namespace Barotrauma
|
||||
targets.Add(target);
|
||||
foreach (ContentXElement subElement in chosenElement.Elements())
|
||||
{
|
||||
LoadTarget(subElement, parentTarget: target);
|
||||
if (subElement.NameAsIdentifier() == "target" ||
|
||||
subElement.NameAsIdentifier() == "chooserandom")
|
||||
{
|
||||
LoadTarget(subElement, parentTarget: target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,6 +373,17 @@ namespace Barotrauma
|
||||
#if SERVER
|
||||
spawnInfo.Clear();
|
||||
#endif
|
||||
if (!IsClient)
|
||||
{
|
||||
// First spawn any possible characters, so that we can use their items as targets.
|
||||
Target firstTarget = targets.First();
|
||||
var submarine = Submarine.Loaded.Find(s => IsValidSubmarine(s, firstTarget.SpawnPositionType));
|
||||
if (submarine != null)
|
||||
{
|
||||
InitCharacters(submarine);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
bool usedExistingItem = false;
|
||||
@@ -376,39 +423,36 @@ namespace Barotrauma
|
||||
case Level.PositionType.Cave:
|
||||
case Level.PositionType.MainPath:
|
||||
case Level.PositionType.SidePath:
|
||||
case Level.PositionType.AbyssCave:
|
||||
target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
|
||||
#if SERVER
|
||||
usedExistingItem = target.Item != null;
|
||||
#endif
|
||||
break;
|
||||
case Level.PositionType.Abyss:
|
||||
target.Item = suitableItems.FirstOrDefault(it => Level.IsPositionInAbyss(it.WorldPosition));
|
||||
break;
|
||||
case Level.PositionType.Ruin:
|
||||
case Level.PositionType.Wreck:
|
||||
case Level.PositionType.Outpost:
|
||||
case Level.PositionType.BeaconStation:
|
||||
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 (it.Submarine is not Submarine sub) { continue; }
|
||||
if (!IsValidSubmarine(sub, target.SpawnPositionType)) { continue; }
|
||||
Rectangle worldBorders = sub.Borders;
|
||||
worldBorders.Location += sub.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;
|
||||
}
|
||||
#if SERVER
|
||||
usedExistingItem = target.Item != null;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (target.Item == null)
|
||||
@@ -460,19 +504,7 @@ namespace Barotrauma
|
||||
{
|
||||
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;
|
||||
}
|
||||
if (!IsValidSubmarine(it.Submarine, target.SpawnPositionType)) { continue; }
|
||||
var itemContainer = it.GetComponent<ItemContainer>();
|
||||
if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); }
|
||||
}
|
||||
@@ -534,6 +566,26 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidSubmarine(Submarine sub, Level.PositionType spawnPosType)
|
||||
{
|
||||
if (sub == null)
|
||||
{
|
||||
return spawnPosType switch
|
||||
{
|
||||
Level.PositionType.Ruin or Level.PositionType.Wreck or Level.PositionType.BeaconStation or Level.PositionType.Outpost => false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
return spawnPosType switch
|
||||
{
|
||||
Level.PositionType.Ruin => sub.Info.IsRuin,
|
||||
Level.PositionType.Wreck => sub.Info.IsWreck,
|
||||
Level.PositionType.BeaconStation => sub.Info.IsBeacon,
|
||||
Level.PositionType.Outpost => sub.Info.IsOutpost,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
@@ -567,48 +619,45 @@ namespace Barotrauma
|
||||
switch (target.State)
|
||||
{
|
||||
case Target.RetrievalState.None:
|
||||
if (target.Interacted)
|
||||
{
|
||||
if (target.Interacted)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.Interact);
|
||||
}
|
||||
var root = target.Item?.RootContainer ?? target.Item;
|
||||
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.PickedUp);
|
||||
}
|
||||
if (inPlayerSub)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
TrySetRetrievalState(Target.RetrievalState.Interact);
|
||||
}
|
||||
var root = target.Item?.RootContainer ?? target.Item;
|
||||
if (root.ParentInventory?.Owner is Character { TeamID: CharacterTeamType.Team1 })
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.PickedUp);
|
||||
#if CLIENT
|
||||
TryShowPickedUpMessage();
|
||||
#endif
|
||||
}
|
||||
if (inPlayerSub)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
break;
|
||||
case Target.RetrievalState.PickedUp:
|
||||
case Target.RetrievalState.RetrievedToSub:
|
||||
bool inPlayerInventory = false;
|
||||
bool playerInFriendlySub = false;
|
||||
if (rootInventoryOwner is Character { TeamID: CharacterTeamType.Team1 } character)
|
||||
{
|
||||
|
||||
bool inPlayerInventory = false;
|
||||
bool playerInFriendlySub = false;
|
||||
if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1)
|
||||
inPlayerInventory = true;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
inPlayerInventory = true;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
playerInFriendlySub =
|
||||
character.IsInFriendlySub ||
|
||||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.State = Target.RetrievalState.PickedUp;
|
||||
playerInFriendlySub =
|
||||
character.IsInFriendlySub ||
|
||||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
|
||||
}
|
||||
}
|
||||
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.State = Target.RetrievalState.PickedUp;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -617,7 +666,7 @@ namespace Barotrauma
|
||||
if (retrievalState < target.State || target.State == retrievalState) { return; }
|
||||
bool wasRetrieved = target.Retrieved;
|
||||
target.State = retrievalState;
|
||||
//increment the mission state if the target became retrieved
|
||||
//increment the mission state if the target became retrieved
|
||||
if (!wasRetrieved && target.Retrieved)
|
||||
{
|
||||
State = Math.Max(i + 1, State);
|
||||
@@ -641,7 +690,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (requiredDeliveryAmount < 1.0f)
|
||||
{
|
||||
return targets.Count(t => IsTargetRetrieved(t)) / (float)targets.Count >= requiredDeliveryAmount;
|
||||
return targets.Count(IsTargetRetrieved) / (float)targets.Count >= requiredDeliveryAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -675,7 +724,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (var target in targetsToRemove)
|
||||
{
|
||||
if (target.Item != null && !target.Item.Removed)
|
||||
if (target.Item is { Removed: false })
|
||||
{
|
||||
target.Item.Remove();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.RuinGeneration;
|
||||
@@ -20,23 +21,14 @@ namespace Barotrauma
|
||||
private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
|
||||
private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
|
||||
private readonly float minTargetDistance;
|
||||
|
||||
|
||||
|
||||
private Ruin TargetRuin { get; set; }
|
||||
|
||||
private bool AllTargetsScanned
|
||||
{
|
||||
get
|
||||
{
|
||||
return scanTargets.Any() && scanTargets.All(kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (State > 0 || scanTargets.None())
|
||||
if (AllTargetsScanned())
|
||||
{
|
||||
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
|
||||
}
|
||||
@@ -234,24 +226,19 @@ namespace Barotrauma
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient) { return; }
|
||||
switch (State)
|
||||
{
|
||||
case 0:
|
||||
if (AllTargetsScanned)
|
||||
{
|
||||
State = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Allow the state to be set higher with MissionStateAction, but not lower.
|
||||
State = Math.Max(State, scanTargets.Count(kvp => kvp.Value));
|
||||
}
|
||||
|
||||
protected override bool DetermineCompleted() => State > 0;
|
||||
|
||||
private bool AllTargetsScanned() => State >= targetsToScan;
|
||||
|
||||
protected override bool DetermineCompleted() => AllTargetsScanned();
|
||||
|
||||
protected override void EndMissionSpecific(bool completed)
|
||||
{
|
||||
foreach (var scanner in scanners)
|
||||
{
|
||||
if (scanner.Item != null && !scanner.Item.Removed)
|
||||
if (scanner.Item is { Removed: false })
|
||||
{
|
||||
scanner.OnScanStarted -= OnScanStarted;
|
||||
scanner.OnScanCompleted -= OnScanCompleted;
|
||||
@@ -259,7 +246,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
Reset();
|
||||
failed = !completed && state > 0;
|
||||
failed = !completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@ namespace Barotrauma
|
||||
/// Maximum number of the specific type of monster in the entire level. Can be used to prevent the event from spawning more monsters if there's
|
||||
/// already enough of that type of monster, e.g. spawned by another event or by a mission.
|
||||
/// </summary>
|
||||
public readonly int MaxAmountPerLevel = int.MaxValue;
|
||||
public readonly int MaxAmountPerLevel;
|
||||
|
||||
private readonly float? overridePlayDeadProbability;
|
||||
|
||||
public IReadOnlyList<Character> Monsters => monsters;
|
||||
public Vector2? SpawnPos => spawnPos;
|
||||
@@ -137,6 +139,11 @@ namespace Barotrauma
|
||||
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
|
||||
delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
|
||||
resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0);
|
||||
float playDeadProbability = prefab.ConfigElement.GetAttributeFloat("playdeadprobability", -1f);
|
||||
if (playDeadProbability >= 0)
|
||||
{
|
||||
overridePlayDeadProbability = playDeadProbability;
|
||||
}
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
@@ -153,9 +160,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static Submarine GetReferenceSub()
|
||||
private static Submarine GetReferenceSub(bool acceptRemoteControlledSubs)
|
||||
{
|
||||
return EventManager.GetRefEntity() as Submarine ?? Submarine.MainSub;
|
||||
return EventManager.GetRefEntity(acceptRemoteControlledSubs) as Submarine ?? Submarine.MainSub;
|
||||
}
|
||||
|
||||
public override IEnumerable<ContentFile> GetFilesToPreload()
|
||||
@@ -175,6 +182,18 @@ namespace Barotrauma
|
||||
|
||||
protected override void InitEventSpecific(EventSet parentSet)
|
||||
{
|
||||
// apply pvp stun resistance (reduce stun amount via resist multiplier)
|
||||
if (GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && !networkMember.ServerSettings.PvPSpawnMonsters)
|
||||
{
|
||||
if (GameSettings.CurrentConfig.VerboseLogging)
|
||||
{
|
||||
DebugConsole.NewMessage($"PvP setting: disabling monster event ({SpeciesName})", Color.Yellow);
|
||||
}
|
||||
|
||||
disallowed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentSet != null && resetTime == 0)
|
||||
{
|
||||
// Use the parent reset time only if there's no reset time defined for the event.
|
||||
@@ -200,13 +219,9 @@ namespace Barotrauma
|
||||
disallowed = true;
|
||||
continue;
|
||||
}
|
||||
if (GameMain.GameSession.IsCurrentLocationRadiated())
|
||||
if (overridePlayDeadProbability.HasValue)
|
||||
{
|
||||
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
|
||||
Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength);
|
||||
createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction);
|
||||
// TODO test multiplayer
|
||||
createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false);
|
||||
createdCharacter.EvaluatePlayDeadProbability(overridePlayDeadProbability);
|
||||
}
|
||||
createdCharacter.DisabledByEvent = true;
|
||||
monsters.Add(createdCharacter);
|
||||
@@ -284,11 +299,18 @@ namespace Barotrauma
|
||||
disallowed = true;
|
||||
return;
|
||||
}
|
||||
Submarine refSub = GetReferenceSub();
|
||||
Submarine refSub = GetReferenceSub(acceptRemoteControlledSubs: true);
|
||||
if (Submarine.MainSubs.Length == 2 && Submarine.MainSubs[1] != null)
|
||||
{
|
||||
refSub = Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced);
|
||||
}
|
||||
//if the reference sub is not the main sub, e.g. a remotely controlled drone,
|
||||
//there's a 50% chance that the monsters will spawn near the main sub instead
|
||||
//so you can't abuse the remotely controlled subs to make monsters only spawn somewhere far away from the main sub
|
||||
if (refSub != Submarine.MainSub && Rand.Range(0.0f, 1.0f) < 0.5f)
|
||||
{
|
||||
refSub ??= GetReferenceSub(acceptRemoteControlledSubs: false);
|
||||
}
|
||||
float closestDist = float.PositiveInfinity;
|
||||
//find the closest spawnposition that isn't too close to any of the subs
|
||||
foreach (var position in availablePositions)
|
||||
@@ -299,7 +321,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (sub.Info.Type != SubmarineType.Player &&
|
||||
sub.Info.Type != SubmarineType.EnemySubmarine &&
|
||||
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
|
||||
!sub.IsRespawnShuttle)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -606,7 +628,7 @@ namespace Barotrauma
|
||||
bool anyInAbyss = false;
|
||||
foreach (Submarine submarine in Submarine.Loaded)
|
||||
{
|
||||
if (submarine.Info.Type != SubmarineType.Player || submarine == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
|
||||
if (submarine.Info.Type != SubmarineType.Player || submarine.IsRespawnShuttle) { continue; }
|
||||
if (submarine.WorldPosition.Y < 0)
|
||||
{
|
||||
anyInAbyss = true;
|
||||
@@ -722,7 +744,9 @@ namespace Barotrauma
|
||||
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI?.CombatStrength ?? 0))}.", Color.LightBlue, debugOnly: true);
|
||||
}
|
||||
|
||||
if (GameMain.GameSession != null)
|
||||
if (GameMain.GameSession != null &&
|
||||
monster.ContentPackage == ContentPackageManager.VanillaCorePackage &&
|
||||
GameAnalyticsManager.ShouldLogRandomSample())
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(
|
||||
$"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}",
|
||||
|
||||
@@ -37,9 +37,11 @@ namespace Barotrauma
|
||||
|
||||
public readonly OnRoundEndAction OnRoundEndAction;
|
||||
|
||||
private readonly string[] requiredDestinationTypes;
|
||||
private readonly Identifier[] requiredDestinationTypes;
|
||||
public readonly bool RequireBeaconStation;
|
||||
|
||||
public readonly Identifier RequiredDestinationFaction;
|
||||
|
||||
public int CurrentActionIndex { get; private set; }
|
||||
public List<EventAction> Actions { get; } = new List<EventAction>();
|
||||
public Dictionary<Identifier, List<Entity>> Targets { get; } = new Dictionary<Identifier, List<Entity>>();
|
||||
@@ -78,17 +80,84 @@ namespace Barotrauma
|
||||
contentPackage: prefab.ContentPackage);
|
||||
}
|
||||
|
||||
requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
|
||||
requiredDestinationTypes = prefab.ConfigElement.GetAttributeIdentifierArray("requireddestinationtypes", Array.Empty<Identifier>());
|
||||
RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
|
||||
RequiredDestinationFaction = prefab.ConfigElement.GetAttributeIdentifier(nameof(RequiredDestinationFaction), Identifier.Empty);
|
||||
|
||||
var allActionsWithIndent = GetAllActions();
|
||||
var allActions = allActionsWithIndent.Select(a => a.action);
|
||||
|
||||
//attempt to check if the event has ConversationActions with options that don't close the prompt and don't lead to any follow-up conversation
|
||||
foreach (var action in allActions)
|
||||
{
|
||||
if (action is ConversationAction conversationAction && conversationAction.Options.Any())
|
||||
{
|
||||
int thisActionIndex = allActionsWithIndent.FindIndex(a => a.action == action);
|
||||
int thisIndentationLevel = allActionsWithIndent[thisActionIndex].indent;
|
||||
bool isLast = true;
|
||||
|
||||
//go through all the actions after this one
|
||||
foreach (var actionWithIndent in allActionsWithIndent.Skip(thisActionIndex + 1))
|
||||
{
|
||||
//if it's an action with the same indentation level, it means it's a ConversationAction coming after this one
|
||||
if (actionWithIndent.action is ConversationAction && actionWithIndent.indent == thisIndentationLevel)
|
||||
{
|
||||
isLast = false;
|
||||
break;
|
||||
}
|
||||
//if the indentation level went back down, we've already searched everything inside this ConversationAction
|
||||
if (actionWithIndent.indent < thisIndentationLevel) { break; }
|
||||
}
|
||||
if (isLast)
|
||||
{
|
||||
foreach (var option in conversationAction.Options)
|
||||
{
|
||||
if (!conversationAction.GetEndingOptions().Contains(conversationAction.Options.IndexOf(option)) &&
|
||||
option.Actions.None(a =>
|
||||
a is ConversationAction || HasConversationSubAction(a) ||
|
||||
/* if there's a goto action explicitly set to end the conversation, assume it's intentional*/
|
||||
a is GoTo { EndConversation: false }))
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in event \"{prefab.Identifier}\": {nameof(ConversationAction)} ({conversationAction.Text}) has an option ({option.Text}) that doesn't end the conversation, but could not find any follow-ups to the conversation.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool HasConversationSubAction(EventAction action)
|
||||
{
|
||||
foreach (var subAction in action.GetSubActions())
|
||||
{
|
||||
if (subAction is ConversationAction) { return true; }
|
||||
if (HasConversationSubAction(subAction)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var label in allActions.OfType<Label>())
|
||||
{
|
||||
if (allActions.None(a => a is GoTo gotoAction && label.Name == gotoAction.Name))
|
||||
{
|
||||
//this can be safe, because a label with no gotos leading to it does nothing (but it's still a sign that something's misconfigured)
|
||||
DebugConsole.AddWarning($"Error in event \"{prefab.Identifier}\". Could not find a GoTo matching the Label \"{label.Name}\".",
|
||||
contentPackage: prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
var allActions = GetAllActions().Select(a => a.action);
|
||||
foreach (var gotoAction in allActions.OfType<GoTo>())
|
||||
{
|
||||
if (allActions.None(a => a is Label label && label.Name == gotoAction.Name))
|
||||
int labelCount = allActions.Count(a => a is Label label && label.Name == gotoAction.Name);
|
||||
if (labelCount == 0)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\".",
|
||||
contentPackage: prefab.ContentPackage);
|
||||
}
|
||||
else if (labelCount > 1)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Multiple labels with the name \"{gotoAction.Name}\".",
|
||||
contentPackage: prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start");
|
||||
@@ -143,7 +212,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all actions in the ScriptedEvent (recursively going through the subactions as well).
|
||||
/// Finds all actions in the ScriptedEvent using a depth-first search (recursively going through the subactions as well).
|
||||
/// Returns a list of tuples where the first value is the indentation level (or "how deep in the hierarchy") the action is.
|
||||
/// </summary>
|
||||
public List<(int indent, EventAction action)> GetAllActions()
|
||||
@@ -433,20 +502,25 @@ namespace Barotrauma
|
||||
|
||||
public override bool LevelMeetsRequirements()
|
||||
{
|
||||
if (requiredDestinationTypes == null) { return true; }
|
||||
var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation;
|
||||
if (currLocation?.Connections == null) { return true; }
|
||||
foreach (LocationConnection c in currLocation.Connections)
|
||||
{
|
||||
if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; }
|
||||
if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier == t))
|
||||
|
||||
var otherLocation = c.OtherLocation(currLocation);
|
||||
if (!RequiredDestinationFaction.IsEmpty && otherLocation.Faction?.Prefab.Identifier != RequiredDestinationFaction) { continue; }
|
||||
|
||||
if (requiredDestinationTypes.Contains(Tags.AnyOutpost) && otherLocation.HasOutpost() && otherLocation.Type.IsAnyOutpost) { return true; }
|
||||
if (requiredDestinationTypes.Any(t => otherLocation.Type.Identifier == t))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return RequiredDestinationFaction.IsEmpty && requiredDestinationTypes.None();
|
||||
}
|
||||
|
||||
|
||||
public override void Finish()
|
||||
{
|
||||
base.Finish();
|
||||
|
||||
Reference in New Issue
Block a user