OBT/1.2.0(Spring Update)

Sync with Upstream
This commit is contained in:
NotAlwaysTrue
2026-04-25 13:25:41 +08:00
committed by GitHub
parent 5207b381b7
commit 59bc21973a
421 changed files with 24090 additions and 11391 deletions
@@ -12,7 +12,11 @@ namespace Barotrauma
public readonly int RandomSeed;
protected readonly EventPrefab prefab;
#nullable enable
public Mission? TriggeringMission;
#nullable restore
public EventPrefab Prefab => prefab;
public EventSet ParentSet { get; private set; }
@@ -29,6 +29,9 @@ namespace Barotrauma
[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; }
[Serialize(true, IsPropertySaveable.Yes, description: "Should the check fail if no targets matching the specified tag are found?")]
public bool FailIfTargetNotFound { get; set; }
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (TargetTag.IsEmpty)
@@ -79,11 +82,10 @@ namespace Barotrauma
if (targets.None())
{
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventDebugName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
contentPackage: ParentEvent.Prefab.ContentPackage);
return !FailIfTargetNotFound;
}
if (targets.None() || Conditionals.None())
if (Conditionals.None())
{
foreach (var target in targets)
{
@@ -14,6 +14,33 @@ namespace Barotrauma
/// </summary>
partial class ConversationAction : EventAction
{
public class OptionActionGroup : SubactionGroup
{
[Serialize("", IsPropertySaveable.Yes, description: "The text to display in the option.")]
public string Text { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "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.")]
public bool EndConversation { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the player will send the {nameof(Text)} in chat when selecting the option, or if {nameof(ForceSayText)} is not empty, will send that instead.")]
public bool ForceSay { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat will be sent in radio chat instead.")]
public bool ForceSayInRadio { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat, if empty, {nameof(Text)} is used instead.")]
public string ForceSayText { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")]
public bool ForceSayRemoveQuotes { get; set; }
public OptionActionGroup(ScriptedEvent scriptedEvent, ContentXElement element) : base(scriptedEvent, element)
{
}
}
public enum DialogTypes
{
@@ -33,6 +60,18 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")]
public string Text { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: $"If enabled, the speaker will send the {nameof(Text)} in chat, or if {nameof(ForceSayText)} is not empty, will send that instead. Note: requires a valid SpeakerTag to be defined.")]
public bool ForceSay { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the message sent in chat by the speaker will be sent in radio chat instead.")]
public bool ForceSayInRadio { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: $"Message sent in chat by the speaker, if empty, {nameof(Text)} is used instead.")]
public string ForceSayText { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Should the chat message be stripped of any quotation mark characters?")]
public bool ForceSayRemoveQuotes { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")]
public Identifier SpeakerTag { get; set; }
@@ -75,7 +114,7 @@ namespace Barotrauma
private AIObjective prevIdleObjective, prevGotoObjective;
private AIObjective npcWaitObjective;
public List<SubactionGroup> Options { get; private set; }
public List<OptionActionGroup> Options { get; private set; }
public SubactionGroup Interrupted { get; private set; }
@@ -99,12 +138,12 @@ namespace Barotrauma
{
actionCount++;
Identifier = actionCount;
Options = new List<SubactionGroup>();
Options = new List<OptionActionGroup>();
foreach (var elem in element.Elements())
{
if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase))
{
Options.Add(new SubactionGroup(ParentEvent, elem));
Options.Add(new OptionActionGroup(ParentEvent, elem));
}
else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase))
{
@@ -215,6 +254,10 @@ namespace Barotrauma
interrupt = false;
dialogOpened = false;
Speaker = null;
#if CLIENT
dialogBox?.Close();
dialogBox = null;
#endif
}
/// <summary>
@@ -292,6 +335,7 @@ namespace Barotrauma
if (dialogOpened)
{
lastActiveTime = Timing.TotalTime;
#if CLIENT
if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction"))
{
@@ -350,7 +394,7 @@ namespace Barotrauma
}
else
{
TryStartConversation(null);
TryStartConversation(Speaker);
}
}
else
@@ -467,11 +511,26 @@ namespace Barotrauma
ParentEvent.AddTarget(InvokerTag, targetCharacter);
}
ShowDialog(speaker, targetCharacter);
if (ForceSay)
{
speaker?.ForceSay(
ForceSayText.IsNullOrEmpty() ? TextManager.Get(Text).Fallback(Text) : TextManager.Get(ForceSayText).Fallback(ForceSayText),
ForceSayInRadio,
ForceSayRemoveQuotes,
// Small delay so the speaking character doesn't talk at the same time as the player
delay: 0.7f);
}
ShowDialog(Speaker, targetCharacter);
dialogOpened = true;
if (speaker != null)
if (Speaker != null)
{
Speaker = speaker;
// Set the Speaker of the child conversation actions so they know which character is speaking
Options.SelectMany(static op => op.Actions).OfType<ConversationAction>().ForEach(action => action.Speaker = speaker);
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.SetCustomInteract(null, null);
#if SERVER
@@ -99,6 +99,7 @@ namespace Barotrauma
else
{
int compareToTargetCount = ParentEvent.GetTargets(CompareToTarget).Count();
if (compareToTargetCount == 0) { return false; }
float percentage = MathUtils.Percentage(targetCount, compareToTargetCount);
if (MinPercentageRelativeToTarget > -1 && percentage < MinPercentageRelativeToTarget) { return false; }
if (MaxPercentageRelativeToTarget > -1 && percentage > MaxPercentageRelativeToTarget) { return false; }
@@ -9,14 +9,7 @@ namespace Barotrauma
{
public class SubactionGroup
{
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;
public EventAction CurrentSubAction
@@ -31,17 +24,17 @@ namespace Barotrauma
}
}
public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem)
public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement element)
{
Text = elem.GetAttribute("text")?.Value ?? "";
SerializableProperty.DeserializeProperties(this, element);
Actions = new List<EventAction>();
EndConversation = elem.GetAttributeBool("endconversation", false);
foreach (var e in elem.Elements())
foreach (var e in element.Elements())
{
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.",
contentPackage: elem.ContentPackage);
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.",
contentPackage: element.ContentPackage);
continue;
}
var action = Instantiate(scriptedEvent, e);
@@ -0,0 +1,62 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Forces a specific character to say a message in chat.
/// </summary>
class ForceSayAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the character that should say the message.")]
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "The message that the character should say. Can be the text as-is, or a tag referring to a line in a text file.")]
public string Message { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the message that the character says be sent in radio?")]
public bool SayInRadio { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Should the message be stripped of any quotation mark characters?")]
public bool RemoveQuotes { get; set; }
public ForceSayAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag);
LocalizedString messageToSay = TextManager.Get(Message).Fallback(Message);
foreach (var target in targets)
{
if (target != null && target is Character character)
{
character.ForceSay(messageToSay, SayInRadio, RemoveQuotes);
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ForceSayAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"Message: {Message})";
}
}
}
@@ -1,84 +1,77 @@
namespace Barotrauma
#nullable enable
namespace Barotrauma;
/// <summary>Changes the state of missions. The way the states are used depends on the type of mission.</summary>
internal sealed class MissionStateAction : EventAction
{
/// <summary>
/// Changes the state of a specific active mission. The way the states are used depends on the type of mission.
/// </summary>
class MissionStateAction : EventAction
/// <summary>The operation to perform on missions' states.</summary>
public enum OperationType
{
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission whose state to change.")]
public Identifier MissionIdentifier { get; set; }
/// <summary>Sets the missions' states to <see cref="State"/>.</summary>
Set,
/// <summary>Adds <see cref="State"/> to the missions' states.</summary>
Add
}
public enum OperationType
[Serialize("", IsPropertySaveable.Yes, "Identifiers of the missions whose states to change. Leave blank to only set the state of the mission that triggered the parent event.")]
public Identifier MissionIdentifier { get; set; }
[Serialize(OperationType.Set, IsPropertySaveable.Yes, "The operation to perform on missions' states.")]
public OperationType Operation { get; set; }
[Serialize(0, IsPropertySaveable.Yes, "The value to apply to missions' states.")]
public int State { get; set; }
[Serialize(false, IsPropertySaveable.Yes, "If set to true, missions are forced to fail without a chance of retrying them.")]
public bool ForceFailure { get; set; }
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
State = element.GetAttributeInt("value", State);
if (Operation == OperationType.Add && State == 0 && !ForceFailure)
{
Set,
Add
DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to only add 0 to the mission state, which will do nothing.",
contentPackage: element.ContentPackage);
}
}
[Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Should the value be added to the state of the mission, or should the state be set to the specified value.")]
public OperationType Operation { get; set; }
private bool isFinished;
public override bool IsFinished(ref string goTo) => isFinished;
public override void Reset() => isFinished = false;
[Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")]
public int State { get; set; }
public override void Update(float deltaTime)
{
if (isFinished) { return; }
[Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the mission is forced to fail without a chance of retrying it.")]
public bool ForceFailure { get; set; }
private bool isFinished;
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
if (!MissionIdentifier.IsEmpty)
{
State = element.GetAttributeInt("value", State);
if (MissionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.",
contentPackage: element.ContentPackage);
}
if (Operation == OperationType.Add && State == 0 && !ForceFailure)
{
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)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
foreach (Mission mission in GameMain.GameSession.Missions)
{
if (mission.Prefab.Identifier != MissionIdentifier) { continue; }
if (ForceFailure)
{
mission.ForceFailure = true;
}
switch (Operation)
{
case OperationType.Set:
mission.State = State;
break;
case OperationType.Add:
mission.State += State;
break;
}
SetMissionState(mission);
}
isFinished = true;
}
else if (ParentEvent.TriggeringMission != null)
{
SetMissionState(ParentEvent.TriggeringMission);
}
public override string ToDebugString()
isFinished = true;
}
private void SetMissionState(Mission mission)
{
if (ForceFailure) { mission.ForceFailure = true; }
switch (Operation)
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})";
case OperationType.Set:
mission.State = State;
break;
case OperationType.Add:
mission.State += State;
break;
}
}
public override string ToDebugString() => $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})";
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -18,6 +18,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop following the target?")]
public bool Follow { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be forced to walk towards the target?")]
public bool ForceWalk { get; set; }
[Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs to target (e.g. you could choose to only make a specific number of security officers follow the player.)")]
public int MaxTargets { get; set; }
@@ -65,7 +68,8 @@ namespace Barotrauma
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = Priority,
IsFollowOrder = true
IsFollowOrder = true,
ForceWalkPermanently = ForceWalk
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -271,6 +271,10 @@ namespace Barotrauma
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
if (newCharacter is { AIController: EnemyAIController enemyAi, Submarine: Submarine ownSub })
{
enemyAi.SetUnattackableSubmarines(ownSub);
}
});
}
}
@@ -240,42 +240,45 @@ namespace Barotrauma
CreateEvents(eventSet);
}
if (level?.LevelData != null)
bool isOutpostLevel = level?.LevelData is { Type: LevelData.LevelType.Outpost } ||
(GameMain.GameSession?.GameMode is TestGameMode && Submarine.MainSub?.Info?.Type == SubmarineType.Outpost);
if (isOutpostLevel)
{
if (level.LevelData.Type == LevelData.LevelType.Outpost)
//if the outpost is connected to a locked connection, create an event to unlock it
if (level?.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
{
//if the outpost is connected to a locked connection, create an event to unlock it
if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
if (unlockPathEventPrefab != null)
{
var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
if (unlockPathEventPrefab != null)
var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed);
activeEvents.Add(newEvent);
}
else
{
//if no event that unlocks the path can be found, unlock it automatically
level.StartLocation.Connections.ForEach(c => c.Locked = false);
}
}
Submarine outpost = level?.StartOutpost ?? Submarine.MainSub;
if (GameMain.NetworkMember is not { IsClient: true } && outpost != null)
{
foreach (var eventTag in outpost.Info.TriggerOutpostMissionEvents)
{
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, outpost.ContentPackage);
if (eventPrefab == null)
{
var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed);
activeEvents.Add(newEvent);
DebugConsole.ThrowError($"Outpost {outpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: outpost.ContentPackage);
}
else
{
//if no event that unlocks the path can be found, unlock it automatically
level.StartLocation.Connections.ForEach(c => c.Locked = false);
var newEvent = eventPrefab.CreateInstance(RandomSeed);
ActivateEvent(newEvent);
}
}
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);
}
}
}
}
}
}
if (level?.LevelData != null)
{
RegisterNonRepeatableChildEvents(initialEventSet);
void RegisterNonRepeatableChildEvents(EventSet eventSet)
{
@@ -233,7 +233,7 @@ namespace Barotrauma
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return State > 0 && State != HostagesKilledState;
}
@@ -171,7 +171,7 @@ namespace Barotrauma
#endif
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return level.CheckBeaconActive();
}
@@ -331,7 +331,7 @@ namespace Barotrauma
}
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
@@ -204,7 +204,7 @@ namespace Barotrauma
}
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return Winner != CharacterTeamType.None;
}
@@ -0,0 +1,18 @@
#nullable enable
namespace Barotrauma;
/// <summary>
/// Defines a mission where the success and failure are determined solely by its state.
/// Intended to be used alongside <see cref="MissionStateAction"/>.
/// </summary>
internal sealed partial class CustomMission(MissionPrefab prefab, Location[] locations, Submarine sub) : Mission(prefab, locations, sub)
{
public readonly int SuccessState = prefab.ConfigElement.GetAttributeInt(nameof(SuccessState), +1);
public readonly int FailureState = prefab.ConfigElement.GetAttributeInt(nameof(FailureState), -1);
public bool RequireDestinationReached = prefab.ConfigElement.GetAttributeBool(nameof(RequireDestinationReached), false);
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) =>
State == SuccessState &&
(!RequireDestinationReached || transitionType is CampaignMode.TransitionType.ProgressToNextLocation or CampaignMode.TransitionType.ProgressToNextEmptyLocation);
}
@@ -1,4 +1,4 @@
using System;
using System;
using Barotrauma.Extensions;
using Barotrauma.RuinGeneration;
using Microsoft.Xna.Framework;
@@ -199,7 +199,7 @@ namespace Barotrauma
private static bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead;
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ?
campaign.GetAvailableTransition() != CampaignMode.TransitionType.None :
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
@@ -301,7 +301,7 @@ namespace Barotrauma
partial void OnStateChangedProjSpecific();
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return Phase == MissionPhase.BossKilled;
}
@@ -343,7 +343,7 @@ namespace Barotrauma
return character != null && !character.Removed && !character.IsDead;
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
@@ -17,7 +17,7 @@ namespace Barotrauma
}
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
if (Level.Loaded?.Type == LevelData.LevelType.Outpost)
{
@@ -25,7 +25,7 @@ namespace Barotrauma
}
else
{
return Submarine.MainSub is { AtEndExit: true };
return transitionType == CampaignMode.TransitionType.ProgressToNextLocation;
}
}
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
@@ -47,6 +47,12 @@ namespace Barotrauma
}
}
/// <summary>
/// Minerals spawned by the mission. Note that minerals that were already present in the level may have also been used as targets.
/// Each list of items represents a separate cluster of minerals.
/// </summary>
public IEnumerable<List<Item>> SpawnedResources => spawnedResources.Values;
public override LocalizedString SuccessMessage => ModifyMessage(base.SuccessMessage);
public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage);
public override LocalizedString Description => ModifyMessage(description);
@@ -169,7 +175,7 @@ namespace Barotrauma
}
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return EnoughHaveBeenCollected();
}
@@ -401,14 +401,9 @@ namespace Barotrauma
{
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
if (spawnedCharacter.AIController is EnemyAIController enemyAi && submarine != null)
{
enemyAi.UnattackableSubmarines.Add(submarine);
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
foreach (Submarine sub in Submarine.MainSub.DockedTo)
{
enemyAi.UnattackableSubmarines.Add(sub);
}
enemyAi.SetUnattackableSubmarines(submarine);
}
InitCharacter(spawnedCharacter, element);
return spawnedCharacter;
@@ -532,6 +527,7 @@ namespace Barotrauma
if (GameMain.GameSession?.EventManager != null)
{
var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
newEvent.TriggeringMission = this;
GameMain.GameSession.EventManager.ActivateEvent(newEvent);
}
}
@@ -539,13 +535,13 @@ namespace Barotrauma
/// <summary>
/// End the mission and give a reward if it was completed successfully
/// </summary>
public void End()
public void End(CampaignMode.TransitionType transitionType)
{
if (GameMain.NetworkMember is not { IsClient: true })
{
completed =
!ForceFailure &&
DetermineCompleted() &&
DetermineCompleted(transitionType) &&
(completeCheckDataAction == null || completeCheckDataAction.GetSuccess());
}
if (completed)
@@ -578,7 +574,7 @@ namespace Barotrauma
}
}
protected abstract bool DetermineCompleted();
protected abstract bool DetermineCompleted(CampaignMode.TransitionType transitionType);
protected virtual void EndMissionSpecific(bool completed) { }
@@ -30,7 +30,8 @@ namespace Barotrauma
{ "GoTo".ToIdentifier(), typeof(GoToMission) },
{ "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) },
{ "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) },
{ "End".ToIdentifier(), typeof(EndMission) }
{ "End".ToIdentifier(), typeof(EndMission) },
{ "Custom".ToIdentifier(), typeof(CustomMission) }
};
/// <summary>
@@ -64,6 +65,7 @@ namespace Barotrauma
public Type MissionClass { get; private set; }
public bool CampaignOnly { get; private set; }
public bool MultiplayerOnly { get; private set; }
public bool SingleplayerOnly { get; private set; }
@@ -319,8 +321,9 @@ namespace Barotrauma
SonarIconIdentifier = ConfigElement.GetAttributeIdentifier("sonaricon", "");
MultiplayerOnly = ConfigElement.GetAttributeBool("multiplayeronly", false);
SingleplayerOnly = ConfigElement.GetAttributeBool("singleplayeronly", false);
CampaignOnly = ConfigElement.GetAttributeBool(nameof(CampaignOnly), false);
MultiplayerOnly = ConfigElement.GetAttributeBool(nameof(MultiplayerOnly), false);
SingleplayerOnly = ConfigElement.GetAttributeBool(nameof(SingleplayerOnly), false);
AchievementIdentifier = ConfigElement.GetAttributeIdentifier("achievementidentifier", "");
@@ -543,7 +546,7 @@ namespace Barotrauma
}
/// <summary>
/// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission
/// Returns all mission types that can be selected 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()
@@ -552,6 +555,7 @@ namespace Barotrauma
foreach (var missionPrefab in Prefabs)
{
if (missionPrefab.Commonness <= 0.0f) { continue; }
if (missionPrefab.CampaignOnly) { continue; }
if (missionPrefab.SingleplayerOnly) { continue; }
if (HiddenMissionTypes.Contains(missionPrefab.Type))
{
@@ -242,7 +242,7 @@ namespace Barotrauma
}
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return state > 0;
}
@@ -337,7 +337,7 @@ namespace Barotrauma
return true;
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return AllItemsDestroyedOrRetrieved();
}
@@ -547,7 +547,7 @@ namespace Barotrauma
return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated;
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
return state == 2;
}
@@ -715,7 +715,7 @@ namespace Barotrauma
}
}
protected override bool DetermineCompleted()
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
if (requiredDeliveryAmount < 1.0f)
{
@@ -1,4 +1,4 @@
using System;
using System;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.RuinGeneration;
@@ -17,7 +17,7 @@ namespace Barotrauma
private readonly Dictionary<Item, ushort> parentInventoryIDs = new Dictionary<Item, ushort>();
private readonly Dictionary<Item, int> inventorySlotIndices = new Dictionary<Item, int>();
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
private readonly int targetsToScan;
private readonly int totalTargetsToScan;
private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
private readonly float minTargetDistance;
@@ -44,7 +44,7 @@ namespace Barotrauma
public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
itemConfig = prefab.ConfigElement.GetChildElement("Items");
targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1);
totalTargetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1);
minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f);
}
@@ -77,57 +77,60 @@ namespace Barotrauma
var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false);
ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null);
if (ruinWaypoints.Count < targetsToScan)
if (ruinWaypoints.Count < totalTargetsToScan)
{
DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})",
DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {totalTargetsToScan})",
contentPackage: Prefab.ContentPackage);
return;
}
//the distance we'll use if we otherwise fail to place the targets far enough from each other
//(smallest extent should be large enough to fit the targets and one extra to be safe)
float guaranteedDistance = Math.Min(TargetRuin.Area.Width, TargetRuin.Area.Height) / (totalTargetsToScan + 1);
var availableWaypoints = new List<WayPoint>();
float minTargetDistanceSquared = minTargetDistance * minTargetDistance;
for (int tries = 0; tries < 15; tries++)
const int MaxTries = 15;
for (int tries = 0; tries < MaxTries; tries++)
{
float triesNormalized = tries / (float)(MaxTries - 1); // 0.0 -> 1.0
float desperationFactor = MathF.Pow(triesNormalized, 2);
//try placing the targets the desired minimum distance apart, gradually lowering the distance requirement on each try
float currentMinDistance = MathHelper.Lerp(minTargetDistance, guaranteedDistance, desperationFactor);
float currentMinDistanceSquared = currentMinDistance * currentMinDistance;
scanTargets.Clear();
availableWaypoints.Clear();
availableWaypoints.AddRange(ruinWaypoints);
for (int i = 0; i < targetsToScan; i++)
for (int i = 0; i < totalTargetsToScan; i++)
{
var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient);
scanTargets.Add(selectedWaypoint, false);
availableWaypoints.Remove(selectedWaypoint);
if (i < (targetsToScan - 1))
if (i < (totalTargetsToScan - 1))
{
availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull);
availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared);
availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < currentMinDistanceSquared);
if (availableWaypoints.None())
{
#if DEBUG
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})",
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})",
contentPackage: Prefab.ContentPackage);
#endif
break;
}
}
}
if (scanTargets.Count >= targetsToScan)
if (scanTargets.Count >= totalTargetsToScan)
{
#if DEBUG
DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green);
#endif
break;
}
if ((tries + 1) % 5 == 0)
{
float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance;
minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance;
#if DEBUG
DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow);
#endif
}
}
if (scanTargets.Count < targetsToScan)
if (scanTargets.Count < totalTargetsToScan)
{
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})",
DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {totalTargetsToScan})",
contentPackage: Prefab.ContentPackage);
}
}
@@ -241,9 +244,9 @@ namespace Barotrauma
State = Math.Max(State, scanTargets.Count(kvp => kvp.Value));
}
private bool AllTargetsScanned() => State >= targetsToScan;
private bool AllTargetsScanned() => State >= totalTargetsToScan;
protected override bool DetermineCompleted() => AllTargetsScanned();
protected override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => AllTargetsScanned();
protected override void EndMissionSpecific(bool completed)
{