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
@@ -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);
}
});
}
}