Build 0.20.0.0

This commit is contained in:
Markus Isberg
2022-10-27 17:54:57 +03:00
parent 05c7b1f869
commit edaf4b09fe
197 changed files with 4344 additions and 1773 deletions
@@ -442,8 +442,7 @@ namespace Barotrauma
{ {
foreach (Limb limb in Limbs) foreach (Limb limb in Limbs)
{ {
if (limb == null || limb.IsSevered || limb.ActiveSprite == null) { continue; } if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; }
Vector2 spriteOrigin = limb.ActiveSprite.Origin; Vector2 spriteOrigin = limb.ActiveSprite.Origin;
spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X;
limb.ActiveSprite.Origin = spriteOrigin; limb.ActiveSprite.Origin = spriteOrigin;
@@ -468,7 +467,10 @@ namespace Barotrauma
{ {
var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage); var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage);
float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10); float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10);
SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range); if (!limbJoint.Params.BreakSound.IsNullOrEmpty() && !limbJoint.Params.BreakSound.Equals("none", StringComparison.OrdinalIgnoreCase))
{
SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range);
}
} }
} }
@@ -109,6 +109,22 @@ namespace Barotrauma
set => grainStrength = Math.Max(0, value); set => grainStrength = Math.Max(0, value);
} }
/// <summary>
/// Can be used to set camera shake from status effects
/// </summary>
public float CameraShake
{
get { return Screen.Selected?.Cam?.Shake ?? 0.0f; }
set
{
if (!MathUtils.IsValid(value)) { return; }
if (Screen.Selected?.Cam != null)
{
Screen.Selected.Cam.Shake = value;
}
}
}
private readonly List<ParticleEmitter> bloodEmitters = new List<ParticleEmitter>(); private readonly List<ParticleEmitter> bloodEmitters = new List<ParticleEmitter>();
public IEnumerable<ParticleEmitter> BloodEmitters public IEnumerable<ParticleEmitter> BloodEmitters
{ {
@@ -307,7 +307,7 @@ namespace Barotrauma
{ {
if (!brokenItem.IsInteractable(character)) { continue; } if (!brokenItem.IsInteractable(character)) { continue; }
float alpha = GetDistanceBasedIconAlpha(brokenItem); float alpha = GetDistanceBasedIconAlpha(brokenItem);
if (alpha <= 0.0f) continue; if (alpha <= 0.0f) { continue; }
GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite, GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite,
Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha);
} }
@@ -501,7 +501,7 @@ namespace Barotrauma
info?.ClearSavedStatValues(statType); info?.ClearSavedStatValues(statType);
for (int i = 0; i < savedStatValueCount; i++) for (int i = 0; i < savedStatValueCount; i++)
{ {
string statIdentifier = msg.ReadString(); Identifier statIdentifier = msg.ReadIdentifier();
float statValue = msg.ReadSingle(); float statValue = msg.ReadSingle();
bool removeOnDeath = msg.ReadBoolean(); bool removeOnDeath = msg.ReadBoolean();
info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true);
@@ -1136,6 +1136,17 @@ namespace Barotrauma
}); });
AssignRelayToServer("debugdraw", false); AssignRelayToServer("debugdraw", false);
AssignOnExecute("debugdrawlocalization", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !TextManager.DebugDraw;
}
TextManager.DebugDraw = state;
NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White);
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("togglevoicechatfilters", (string[] args) => AssignOnExecute("togglevoicechatfilters", (string[] args) =>
{ {
if (args.None() || !bool.TryParse(args[0], out bool state)) if (args.None() || !bool.TryParse(args[0], out bool state))
@@ -1696,6 +1707,8 @@ namespace Barotrauma
GameSettings.SetCurrentConfig(config); GameSettings.SetCurrentConfig(config);
} }
HashSet<string> missingTexts = new HashSet<string>();
//key = text tag, value = list of languages the tag is missing from //key = text tag, value = list of languages the tag is missing from
Dictionary<Identifier, HashSet<LanguageIdentifier>> missingTags = new Dictionary<Identifier, HashSet<LanguageIdentifier>>(); Dictionary<Identifier, HashSet<LanguageIdentifier>> missingTags = new Dictionary<Identifier, HashSet<LanguageIdentifier>>();
Dictionary<LanguageIdentifier, HashSet<Identifier>> tags = new Dictionary<LanguageIdentifier, HashSet<Identifier>>(); Dictionary<LanguageIdentifier, HashSet<Identifier>> tags = new Dictionary<LanguageIdentifier, HashSet<Identifier>>();
@@ -1755,20 +1768,38 @@ namespace Barotrauma
foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)))) foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))))
{ {
foreach (var property in itemComponentType.GetProperties()) checkSerializableEntityType(itemComponentType);
}
checkSerializableEntityType(typeof(Item));
checkSerializableEntityType(typeof(Hull));
checkSerializableEntityType(typeof(Structure));
void checkSerializableEntityType(Type t)
{
foreach (var property in t.GetProperties())
{ {
if (!property.IsDefined(typeof(InGameEditable), false)) { continue; } if (!property.IsDefined(typeof(Editable), false)) { continue; }
string propertyTag = $"{property.DeclaringType.Name}.{property.Name}"; string propertyTag = $"{property.DeclaringType.Name}.{property.Name}";
addIfMissingAll(language, if (addIfMissingAll(language,
propertyTag.ToIdentifier(), propertyTag.ToIdentifier(),
property.Name.ToIdentifier(), property.Name.ToIdentifier(),
$"sp.{propertyTag}.name".ToIdentifier()); $"sp.{property.Name}.name".ToIdentifier(),
$"sp.{propertyTag}.name".ToIdentifier()) && language == "English".ToLanguageIdentifier())
{
missingTexts.Add($"<sp.{propertyTag.ToLower()}.name>{property.Name.FormatCamelCaseWithSpaces()}</sp.{propertyTag.ToLower()}.name>");
}
addIfMissingAll(language, var description = (property.GetCustomAttributes(true).First(a => a is Serialize) as Serialize).Description;
if (addIfMissingAll(language,
$"sp.{propertyTag}.description".ToIdentifier(), $"sp.{propertyTag}.description".ToIdentifier(),
$"{property.Name.ToIdentifier()}.description".ToIdentifier()); $"sp.{property.Name}.description".ToIdentifier(),
$"{property.Name.ToIdentifier()}.description".ToIdentifier()) && language == "English".ToLanguageIdentifier())
{
missingTexts.Add($"<sp.{propertyTag.ToLower()}.description>{description}</sp.{propertyTag.ToLower()}.description>");
}
} }
} }
@@ -1889,6 +1920,23 @@ namespace Barotrauma
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
SwapLanguage(TextManager.DefaultLanguage); SwapLanguage(TextManager.DefaultLanguage);
if (missingTexts.Any())
{
ShowQuestionPrompt("Dump the property names and descriptions missing from English to a new xml file? Y/N",
(option) =>
{
if (option.ToLowerInvariant() == "y")
{
string path = "newtexts.txt";
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
File.WriteAllLines(path, missingTexts);
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(path));
SwapLanguage(TextManager.DefaultLanguage);
}
});
}
void addIfMissing(Identifier tag, LanguageIdentifier language) void addIfMissing(Identifier tag, LanguageIdentifier language)
{ {
if (!tags[language].Contains(tag)) if (!tags[language].Contains(tag))
@@ -1897,15 +1945,90 @@ namespace Barotrauma
missingTags[tag].Add(language); missingTags[tag].Add(language);
} }
} }
void addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags) bool addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags)
{ {
if (!potentialTags.Any(t => tags[language].Contains(t))) if (!potentialTags.Any(t => tags[language].Contains(t)))
{ {
var tag = potentialTags.First(); var tag = potentialTags.First();
if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet<LanguageIdentifier>(); } if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet<LanguageIdentifier>(); }
missingTags[tag].Add(language); missingTags[tag].Add(language);
return true;
}
return false;
}
}));
commands.Add(new Command("checkduplicateloca", "", (string[] args) =>
{
if (args.Length < 1)
{
ThrowError("Please specify a file path.");
return;
}
XDocument doc1 = XMLExtensions.TryLoadXml(args[0]);
if (doc1?.Root == null)
{
ThrowError($"Could not load the file \"{args[0]}\"");
return;
}
List<(string tag, string text)> texts = new List<(string tag, string text)>();
bool duplicatesFound = false;
foreach (XElement element in doc1.Root.Elements())
{
string tag = element.Name.ToString();
string text = element.ElementInnerText();
if (texts.Any(t => t.tag == tag))
{
ThrowError($"Duplicate tag \"{tag}\".");
duplicatesFound = true;
} }
} }
if (duplicatesFound)
{
ThrowError($"Aborting, please fix duplicate tags in the file and try again.");
return;
}
foreach (XElement element in doc1.Root.Elements())
{
string tag = element.Name.ToString();
string text = element.ElementInnerText();
if (texts.Any(t => t.text == text))
{
if (tag.StartsWith("sp."))
{
string[] split = tag.Split('.');
if (split.Length > 3)
{
texts.RemoveAll(t => t.text == text);
string newTag = $"sp.{split[2]}.{split[3]}";
texts.Add((newTag, text));
NewMessage($"Duplicate text \"{tag}\", merging to \"{newTag}\".");
}
else
{
NewMessage($"Duplicate text \"{tag}\", using existing one \"{texts.Find(t => t.text == text).tag}\".");
}
}
else
{
texts.Add((tag, text));
ThrowError($"Duplicate text \"{tag}\". Could not determine if the text can be merged with an existing one, please check it manually.");
}
}
else
{
texts.Add((tag, text));
}
}
string filePath = "uniquetexts.xml";
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
File.WriteAllLines(filePath, texts.Select(t => $"<{t.tag}>{t.text}</{t.tag}>"));
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
})); }));
commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) => commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) =>
@@ -2585,99 +2708,6 @@ namespace Barotrauma
})); }));
#endif #endif
commands.Add(new Command("cleanbuild", "", (string[] args) =>
{
/*GameSettings.CurrentConfig.MusicVolume = 0.5f;
GameSettings.CurrentConfig.SoundVolume = 0.5f;
GameSettings.CurrentConfig.DynamicRangeCompressionEnabled = true;
GameSettings.CurrentConfig.VoipAttenuationEnabled = true;
NewMessage("Music and sound volume set to 0.5", Color.Green);
GameSettings.CurrentConfig.GraphicsWidth = 0;
GameSettings.CurrentConfig.GraphicsHeight = 0;
GameSettings.CurrentConfig.WindowMode = WindowMode.BorderlessWindowed;
NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green);
NewMessage("Fullscreen enabled", Color.Green);
GameSettings.CurrentConfig.VerboseLogging = false;
if (GameSettings.CurrentConfig.MasterServerUrl != "http://www.undertowgames.com/baromaster")
{
ThrowError("MasterServerUrl \"" + GameSettings.CurrentConfig.MasterServerUrl + "\"!");
}
GameSettings.SaveCurrentConfig();*/
throw new NotImplementedException();
#warning TODO: reimplement
var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder);
foreach (string saveFile in saveFiles)
{
Barotrauma.IO.File.Delete(saveFile);
NewMessage("Deleted " + saveFile, Color.Green);
}
if (Barotrauma.IO.Directory.Exists(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp")))
{
Barotrauma.IO.Directory.Delete(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true);
NewMessage("Deleted temp save folder", Color.Green);
}
if (Barotrauma.IO.Directory.Exists(ServerLog.SavePath))
{
var logFiles = Barotrauma.IO.Directory.GetFiles(ServerLog.SavePath);
foreach (string logFile in logFiles)
{
Barotrauma.IO.File.Delete(logFile);
NewMessage("Deleted " + logFile, Color.Green);
}
}
if (Barotrauma.IO.File.Exists("filelist.xml"))
{
Barotrauma.IO.File.Delete("filelist.xml");
NewMessage("Deleted filelist", Color.Green);
}
if (Barotrauma.IO.File.Exists("Data/bannedplayers.txt"))
{
Barotrauma.IO.File.Delete("Data/bannedplayers.txt");
NewMessage("Deleted bannedplayers.txt", Color.Green);
}
if (Barotrauma.IO.File.Exists("Submarines/TutorialSub.sub"))
{
Barotrauma.IO.File.Delete("Submarines/TutorialSub.sub");
NewMessage("Deleted TutorialSub from the submarine folder", Color.Green);
}
/*if (Barotrauma.IO.File.Exists(GameServer.SettingsFile))
{
Barotrauma.IO.File.Delete(GameServer.SettingsFile);
NewMessage("Deleted server settings", Color.Green);
}
if (Barotrauma.IO.File.Exists(GameServer.ClientPermissionsFile))
{
Barotrauma.IO.File.Delete(GameServer.ClientPermissionsFile);
NewMessage("Deleted client permission file", Color.Green);
}*/
if (Barotrauma.IO.File.Exists("crashreport.log"))
{
Barotrauma.IO.File.Delete("crashreport.log");
NewMessage("Deleted crashreport.log", Color.Green);
}
if (!Barotrauma.IO.File.Exists("Content/Map/TutorialSub.sub"))
{
ThrowError("TutorialSub.sub not found!");
}
}));
commands.Add(new Command("reloadcorepackage", "", (string[] args) => commands.Add(new Command("reloadcorepackage", "", (string[] args) =>
{ {
if (args.Length < 1) if (args.Length < 1)
@@ -769,7 +769,7 @@ namespace Barotrauma
if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio))
{ {
radio.Channel = channel; radio.Channel = channel;
GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()])); GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()], radio));
if (setText) if (setText)
{ {
@@ -25,7 +25,7 @@ namespace Barotrauma
private PlayerBalanceElement? playerBalanceElement; private PlayerBalanceElement? playerBalanceElement;
private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires;
private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(ClientPermissions.ManageHires); private bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires);
private Point resolutionWhenCreated; private Point resolutionWhenCreated;
@@ -357,6 +357,17 @@ namespace Barotrauma
string txt = directory; string txt = directory;
if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); } if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
if (!txt.EndsWith("/")) { txt += "/"; } if (!txt.EndsWith("/")) { txt += "/"; }
//get directory info
DirectoryInfo dirInfo = new DirectoryInfo(directory);
try
{
//this will throw an exception if the directory can't be opened
Directory.GetDirectories(directory);
}
catch (UnauthorizedAccessException)
{
continue;
}
var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt)
{ {
UserData = ItemIsDirectory.Yes UserData = ItemIsDirectory.Yes
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Barotrauma.IO; using Barotrauma.IO;
using System.Linq; using System.Linq;
using System.Xml.Linq;
using Barotrauma.CharacterEditor; using Barotrauma.CharacterEditor;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
@@ -50,6 +49,14 @@ namespace Barotrauma
static class GUI static class GUI
{ {
// Controls where a line is drawn for given coords.
public enum OutlinePosition
{
Default = 0, // Thickness is inside of top left and outside of bottom right coord
Inside = 1, // Thickness is subtracted from the inside
Centered = 2, // Thickness is centered on given coords
Outside = 3, // Tickness is added to the outside
}
public static GUICanvas Canvas => GUICanvas.Instance; public static GUICanvas Canvas => GUICanvas.Instance;
public static CursorState MouseCursor = CursorState.Default; public static CursorState MouseCursor = CursorState.Default;
@@ -1605,6 +1612,54 @@ namespace Barotrauma
} }
} }
public static void DrawRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 origin, float rotation, Color clr, float depth = 0.0f, float thickness = 1, OutlinePosition outlinePos = OutlinePosition.Centered)
{
Vector2 topLeft = new Vector2(-origin.X, -origin.Y);
Vector2 topRight = new Vector2(-origin.X + size.X, -origin.Y);
Vector2 bottomLeft = new Vector2(-origin.X, -origin.Y + size.Y);
Vector2 actualSize = size;
switch(outlinePos)
{
case OutlinePosition.Default:
actualSize += new Vector2(thickness);
break;
case OutlinePosition.Centered:
topLeft -= new Vector2(thickness * 0.5f);
topRight -= new Vector2(thickness * 0.5f);
bottomLeft -= new Vector2(thickness * 0.5f);
actualSize += new Vector2(thickness);
break;
case OutlinePosition.Inside:
topRight -= new Vector2(thickness, 0.0f);
bottomLeft -= new Vector2(0.0f, thickness);
break;
case OutlinePosition.Outside:
topLeft -= new Vector2(thickness);
topRight -= new Vector2(0.0f, thickness);
bottomLeft -= new Vector2(thickness, 0.0f);
actualSize += new Vector2(thickness * 2.0f);
break;
}
Matrix rotate = Matrix.CreateRotationZ(rotation);
topLeft = Vector2.Transform(topLeft, rotate) + position;
topRight = Vector2.Transform(topRight, rotate) + position;
bottomLeft = Vector2.Transform(bottomLeft, rotate) + position;
Rectangle srcRect = new Rectangle(0, 0, 1, 1);
sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, topRight, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, bottomLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
}
public static void DrawFilledRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 pivot, float rotation, Color clr, float depth = 0.0f)
{
Rectangle srcRect = new Rectangle(0, 0, 1, 1);
sb.Draw(solidWhiteTexture, position, srcRect, clr, rotation, (pivot/size), size, SpriteEffects.None, depth);
}
public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f) public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f)
{ {
DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth); DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth);
@@ -584,6 +584,13 @@ namespace Barotrauma
{ {
string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue);
Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f); Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f);
if (TextManager.DebugDraw)
{
if (!text.NestedStr.Loaded || text.NestedStr.Language == LanguageIdentifier.None)
{
colorToShow = Color.Magenta;
}
}
if (Shadow) if (Shadow)
{ {
@@ -139,8 +139,8 @@ namespace Barotrauma
return tab switch return tab switch
{ {
StoreTab.Buy => true, StoreTab.Buy => true,
StoreTab.Sell => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems), StoreTab.Sell => CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems),
StoreTab.SellSub => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems), StoreTab.SellSub => CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems),
_ => false, _ => false,
}; };
} }
@@ -892,7 +892,7 @@ namespace Barotrauma
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity)
{ {
if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo)) if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy())
{ {
bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab);
var itemFrame = isDailySpecial ? var itemFrame = isDailySpecial ?
@@ -1995,11 +1995,23 @@ namespace Barotrauma
int totalPrice = 0; int totalPrice = 0;
foreach (var item in itemsToPurchase) foreach (var item in itemsToPurchase)
{ {
if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo)) if (item is null) { continue; }
if (item.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo))
{ {
itemsToRemove.Add(item); itemsToRemove.Add(item);
continue; continue;
} }
if (item.ItemPrefab.DefaultPrice.RequiresUnlock)
{
if (!CargoManager.HasUnlockedStoreItem(item.ItemPrefab))
{
itemsToRemove.Add(item);
continue;
}
}
totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo);
} }
itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); itemsToRemove.ForEach(i => itemsToPurchase.Remove(i));
@@ -673,7 +673,7 @@ namespace Barotrauma
{ {
if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null) if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null)
{ {
return Submarine.MainSub.Info; return Submarine.MainSub?.Info;
} }
else else
{ {
@@ -34,7 +34,7 @@ namespace Barotrauma
private List<CharacterTeamType> teamIDs; private List<CharacterTeamType> teamIDs;
private const string inLobbyString = "\u2022 \u2022 \u2022"; private const string inLobbyString = "\u2022 \u2022 \u2022";
private GUIFrame pendingChangesFrame = null; public static GUIFrame PendingChangesFrame = null;
public static Color OwnCharacterBGColor = Color.Gold * 0.7f; public static Color OwnCharacterBGColor = Color.Gold * 0.7f;
private bool isTransferMenuOpen; private bool isTransferMenuOpen;
@@ -44,6 +44,7 @@ namespace Barotrauma
private float transferMenuOpenState; private float transferMenuOpenState;
private bool transferMenuStateCompleted; private bool transferMenuStateCompleted;
private readonly HashSet<Identifier> registeredEvents = new HashSet<Identifier>(); private readonly HashSet<Identifier> registeredEvents = new HashSet<Identifier>();
private readonly TalentMenu talentMenu = new TalentMenu();
private class LinkedGUI private class LinkedGUI
{ {
@@ -206,14 +207,10 @@ namespace Barotrauma
transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height); transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height);
} }
GameSession.UpdateTalentNotificationIndicator(talentPointNotification); GameSession.UpdateTalentNotificationIndicator(talentPointNotification);
if (Character.Controlled?.Info is { } characterInfo && talentResetButton != null && talentApplyButton != null)
if (SelectedTab is InfoFrameTab.Talents)
{ {
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); talentMenu?.Update();
talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
talentApplyButton.Flash(GUIStyle.Orange);
}
} }
if (SelectedTab != InfoFrameTab.Crew) { return; } if (SelectedTab != InfoFrameTab.Crew) { return; }
@@ -251,6 +248,10 @@ namespace Barotrauma
{ {
infoFrame?.AddToGUIUpdateList(); infoFrame?.AddToGUIUpdateList();
NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList();
if (SelectedTab is InfoFrameTab.Talents)
{
talentMenu?.AddToGUIUpdateList();
}
} }
public static void OnRoundEnded() public static void OnRoundEnded()
@@ -325,11 +326,11 @@ namespace Barotrauma
AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8))
}, style: null); }, style: null);
pendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null); PendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null);
if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false) if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false)
{ {
NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); NetLobbyScreen.CreateChangesPendingFrame(PendingChangesFrame);
} }
SetBalanceText(balanceText, campaignMode.Bank.Balance); SetBalanceText(balanceText, campaignMode.Bank.Balance);
@@ -403,7 +404,7 @@ namespace Barotrauma
CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub);
break; break;
case InfoFrameTab.Talents: case InfoFrameTab.Talents:
CreateCharacterInfo(infoFrameHolder); talentMenu.CreateGUI(infoFrameHolder);
break; break;
} }
} }
@@ -1774,370 +1775,10 @@ namespace Barotrauma
sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true);
} }
} }
private Color unselectedColor = new Color(240, 255, 255, 225);
private Color unselectableColor = new Color(100, 100, 100, 225);
private Color pressedColor = new Color(60, 60, 60, 225);
private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>();
private readonly List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>();
private List<Identifier> selectedTalents = new List<Identifier>();
private GUITextBlock experienceText;
private GUIProgressBar experienceBar;
private GUITextBlock talentPointText;
private GUIListBox skillListBox;
private GUIButton talentApplyButton,
talentResetButton;
private GUIImage talentPointNotification; private GUIImage talentPointNotification;
private readonly ImmutableDictionary<TalentTree.TalentTreeStageState, GUIComponentStyle> talentStageStyles = new Dictionary<TalentTree.TalentTreeStageState, GUIComponentStyle> public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
{
{ TalentTree.TalentTreeStageState.Invalid, GUIStyle.GetComponentStyle("TalentTreeLocked") },
{ TalentTree.TalentTreeStageState.Locked, GUIStyle.GetComponentStyle("TalentTreeLocked") },
{ TalentTree.TalentTreeStageState.Unlocked, GUIStyle.GetComponentStyle("TalentTreePurchased") },
{ TalentTree.TalentTreeStageState.Available, GUIStyle.GetComponentStyle("TalentTreeUnlocked") },
{ TalentTree.TalentTreeStageState.Highlighted, GUIStyle.GetComponentStyle("TalentTreeAvailable") },
}.ToImmutableDictionary();
private readonly ImmutableDictionary<TalentTree.TalentTreeStageState, Color> talentStageBackgroundColors = new Dictionary<TalentTree.TalentTreeStageState, Color>
{
{ TalentTree.TalentTreeStageState.Invalid, new Color(48,48,48,255) },
{ TalentTree.TalentTreeStageState.Locked, new Color(48,48,48,255) },
{ TalentTree.TalentTreeStageState.Unlocked, new Color(24,37,31,255) },
{ TalentTree.TalentTreeStageState.Available, new Color(50,47,33,255) },
{ TalentTree.TalentTreeStageState.Highlighted, new Color(50,47,33,255) },
}.ToImmutableDictionary();
private void CreateCharacterInfo(GUIFrame infoFrame)
{
infoFrame.ClearChildren();
talentButtons.Clear();
talentCornerIcons.Clear();
GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = GUI.IntScale(15);
GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null);
GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null);
GUIFrame characterSettingsFrame = null;
GUILayoutGroup characterLayout = null;
if (!(GameMain.NetworkMember is null))
{
characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: null) { Visible = false };
characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform));
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null);
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
}
Character controlledCharacter = Character.Controlled;
CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo;
if (info == null) { return; }
Job job = info.Job;
GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
{
AbsoluteSpacing = GUI.IntScale(10),
Stretch = true
};
GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), contentLayout.RectTransform, Anchor.Center), isHorizontal: true);
new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
{
float posY = component.Rect.Center.Y - component.Rect.Width / 2;
info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false);
});
GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform))
{
AbsoluteSpacing = GUI.IntScale(5),
CanBeFocused = true
};
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
if (!info.OmitJobInMenus)
{
nameBlock.TextColor = job.Prefab.UIColor;
GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor };
}
LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName);
Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString);
GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont);
traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint();
IEnumerable<TalentPrefab> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e));
if (talentsOutsideTree.Count() > 0)
{
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null);
GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter);
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont);
talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y);
var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.8f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true)
{
AutoHideScrollBar = false,
ResizeContentToMakeSpaceForScrollBar = false
};
extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter);
extraTalentList.RectTransform.MinSize = new Point(0, GUI.IntScale(65));
extraTalentLayout.Recalculate();
extraTalentList.ForceLayoutRecalculation();
foreach (var extraTalent in talentsOutsideTree)
{
var img = new GUIImage(new RectTransform(new Point(extraTalentList.Content.Rect.Height), extraTalentList.Content.RectTransform), sprite: extraTalent.Icon, scaleToFit: true)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description),
Color = GUIStyle.Green
};
img.RectTransform.SizeChanged += () =>
{
img.RectTransform.MaxSize = new Point(img.Rect.Height);
};
}
}
GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight)
{
AbsoluteSpacing = GUI.IntScale(5),
Stretch = true
};
GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont);
skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null);
CreateSkillList(controlledCharacter, info, skillListBox);
new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.6f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null);
if (controlledCharacter == null)
{
talentTreeListBox.Enabled = false;
}
else
{
if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
selectedTalents = info.GetUnlockedTalentsInTree().ToList();
List<GUITextBlock> subTreeNames = new List<GUITextBlock>();
foreach (var subTree in talentTree.TalentSubTrees)
{
GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null);
GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter);
GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null);
int elementPadding = GUI.IntScale(8);
Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize;
GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader");
subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center));
for (int i = 0; i < 4; i++)
{
GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null);
Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground")
{
Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked]
};
GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false };
GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null)
{
CanBeFocused = false,
Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked]
};
Point iconSize = cornerIcon.RectTransform.NonScaledSize;
cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2);
if (subTree.TalentOptionStages.Length <= i) { continue; }
TalentOption talentOption = subTree.TalentOptionStages[i];
GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
foreach (Identifier talentId in talentOption.TalentIdentifiers.OrderBy(t => t))
{
if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab talent)) { continue; }
GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null)
{
CanBeFocused = false
};
GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null);
GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + talent.Description),
UserData = talent.Identifier,
PressedColor = pressedColor,
Enabled = controlledCharacter != null,
OnClicked = (button, userData) =>
{
// deselect other buttons in tier by removing their selected talents from pool
foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren<GUIButton>())
{
if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button)
{
if (!controlledCharacter.HasTalent(otherTalentIdentifier))
{
selectedTalents.Remove(otherTalentIdentifier);
}
}
}
Identifier talentIdentifier = (Identifier)userData;
if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents))
{
if (!selectedTalents.Contains(talentIdentifier))
{
selectedTalents.Add(talentIdentifier);
}
}
else if (!controlledCharacter.HasTalent(talentIdentifier))
{
selectedTalents.Remove(talentIdentifier);
}
UpdateTalentInfo();
return true;
},
};
talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
GUIComponent iconImage;
if (talent.Icon is null)
{
iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null)
{
OutlineColor = GUIStyle.Red,
TextColor = GUIStyle.Red,
PressedColor = unselectableColor,
DisabledColor = unselectableColor,
CanBeFocused = false,
};
}
else
{
iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true)
{
PressedColor = unselectableColor,
DisabledColor = unselectableColor * 0.5f,
CanBeFocused = false,
};
}
iconImage.Enabled = talentButton.Enabled;
talentButtons.Add((talentButton, iconImage));
}
talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight));
}
}
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true
};
GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform));
GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null);
experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft),
barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
{
IsHorizontal = true,
};
experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
Shadow = true,
ToolTip = TextManager.Get("experiencetooltip")
};
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true };
talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale")
{
OnClicked = ResetTalentSelection
};
talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale")
{
OnClicked = ApplyTalentSelection,
};
GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
}
if (!(GameMain.NetworkMember is null))
{
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
{
IgnoreLayoutGroups = false
};
newCharacterBox.TextBlock.AutoScaleHorizontal = true;
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{
newCharacterBox.Text = TextManager.Get("settings");
if (pendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame);
}
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
if (!(characterLayout is null))
{
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
{
OnClicked = (button, o) =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
characterSettingsFrame!.Visible = false;
content.Visible = true;
return true;
}
};
}
}
UpdateTalentInfo();
}
private void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
{ {
parent.Content.ClearChildren(); parent.Content.ClearChildren();
List<GUITextBlock> skillNames = new List<GUITextBlock>(); List<GUITextBlock> skillNames = new List<GUITextBlock>();
@@ -2172,116 +1813,10 @@ namespace Barotrauma
GUITextBlock.AutoScaleAndNormalize(skillNames); GUITextBlock.AutoScaleAndNormalize(skillNames);
} }
private void UpdateTalentInfo()
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter?.Info == null) { return; }
if (SelectedTab != InfoFrameTab.Talents) { return; }
bool unlockedAllTalents = controlledCharacter.HasUnlockedAllTalents();
if (unlockedAllTalents)
{
experienceText.Text = string.Empty;
experienceBar.BarSize = 1f;
}
else
{
experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}";
experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel();
}
selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents);
string pointsLeft = controlledCharacter.Info.GetAvailableTalentPoints().ToString();
int talentCount = selectedTalents.Count - controlledCharacter.Info.GetUnlockedTalentsInTree().Count();
if (unlockedAllTalents)
{
talentPointText.SetRichText($"‖color:{XMLExtensions.ToStringHex(Color.Gray)}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖");
}
else if (talentCount > 0)
{
string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUIStyle.Red)}‖{-talentCount}‖color:end‖";
LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed));
talentPointText.SetRichText(localizedString);
}
else
{
talentPointText.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft));
}
foreach (var (talentTree, index, icon, frame, glow) in talentCornerIcons)
{
TalentTree.TalentTreeStageState state = TalentTree.GetTalentOptionStageState(controlledCharacter, talentTree, index, selectedTalents);
GUIComponentStyle newStyle = talentStageStyles[state];
icon.ApplyStyle(newStyle);
icon.Color = newStyle.Color;
frame.Color = talentStageBackgroundColors[state];
glow.Visible = state == TalentTree.TalentTreeStageState.Highlighted;
}
foreach (var talentButton in talentButtons)
{
Identifier talentIdentifier = (Identifier)talentButton.button.UserData;
bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier);
Color newTalentColor = unselectable ? unselectableColor : unselectedColor;
Color hoverColor = Color.White;
if (controlledCharacter.HasTalent(talentIdentifier))
{
newTalentColor = GUIStyle.Green;
}
else if (selectedTalents.Contains(talentIdentifier))
{
newTalentColor = GUIStyle.Orange;
hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f);
}
talentButton.icon.Color = newTalentColor;
talentButton.icon.HoverColor = hoverColor;
}
CreateSkillList(controlledCharacter, controlledCharacter.Info, skillListBox);
}
private void ApplyTalents(Character controlledCharacter)
{
selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents);
foreach (Identifier talent in selectedTalents)
{
controlledCharacter.GiveTalent(talent);
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData());
}
}
selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList();
UpdateTalentInfo();
}
private bool ApplyTalentSelection(GUIButton guiButton, object userData)
{
Character controlledCharacter = Character.Controlled;
ApplyTalents(controlledCharacter);
return true;
}
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter?.Info == null) { return false; }
selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList();
UpdateTalentInfo();
return true;
}
public void OnExperienceChanged(Character character) public void OnExperienceChanged(Character character)
{ {
if (character != Character.Controlled) { return; } if (character != Character.Controlled) { return; }
UpdateTalentInfo(); talentMenu.UpdateTalentInfo();
} }
public void OnClose() public void OnClose()
@@ -0,0 +1,680 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using static Barotrauma.TalentTree;
using static Barotrauma.TalentTree.TalentTreeStageState;
namespace Barotrauma
{
internal readonly record struct TalentButton(GUIButton Button,
GUIComponent IconComponent,
TalentPrefab Prefab)
{
public Identifier Identifier => Prefab.Identifier;
}
internal readonly record struct TalentCornerIcon(Identifier TalentTree,
int Index,
GUIImage IconComponent,
GUIFrame BackgroundComponent,
GUIFrame GlowComponent);
internal readonly struct TalentTreeStyle
{
public readonly GUIComponentStyle ComponentStyle;
public readonly Color Color;
public TalentTreeStyle(string componentStyle, Color color)
{
ComponentStyle = GUIStyle.GetComponentStyle(componentStyle);
Color = color;
}
}
internal sealed class TalentMenu
{
private static readonly Color unselectedColor = new Color(240, 255, 255, 225),
unselectableColor = new Color(100, 100, 100, 225),
pressedColor = new Color(60, 60, 60, 225),
lockedColor = new Color(48, 48, 48, 255),
unlockedColor = new Color(24, 37, 31, 255),
availableColor = new Color(50, 47, 33, 255);
private static readonly ImmutableDictionary<TalentTreeStageState, TalentTreeStyle> talentStageStyles =
new Dictionary<TalentTreeStageState, TalentTreeStyle>
{
[Invalid] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
[Locked] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
[Unlocked] = new TalentTreeStyle("TalentTreePurchased", unlockedColor),
[Available] = new TalentTreeStyle("TalentTreeUnlocked", availableColor),
[Highlighted] = new TalentTreeStyle("TalentTreeAvailable", availableColor)
}.ToImmutableDictionary();
private readonly HashSet<TalentButton> talentButtons = new HashSet<TalentButton>();
private readonly HashSet<GUIComponent> showCaseTalentFrames = new HashSet<GUIComponent>();
private readonly HashSet<TalentCornerIcon> talentCornerIcons = new HashSet<TalentCornerIcon>();
private HashSet<Identifier> selectedTalents = new HashSet<Identifier>();
private GUIListBox? skillListBox;
private GUITextBlock? talentPointText;
private GUIProgressBar? experienceBar;
private GUITextBlock? experienceText;
private GUILayoutGroup? skillLayout;
private GUIButton? talentApplyButton,
talentResetButton;
public void CreateGUI(GUIFrame parent)
{
parent.ClearChildren();
talentButtons.Clear();
talentCornerIcons.Clear();
showCaseTalentFrames.Clear();
GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = GUI.IntScale(15);
GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), parent.RectTransform, Anchor.Center), style: null);
GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null);
GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
{
AbsoluteSpacing = GUI.IntScale(10),
Stretch = true
};
Character? controlledCharacter = Character.Controlled;
CharacterInfo? info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo;
if (info is null) { return; }
CreateStatPanel(contentLayout, info);
new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
if (JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree? talentTree))
{
CreateTalentMenu(contentLayout, info, talentTree!);
}
CreateFooter(contentLayout, info);
UpdateTalentInfo();
if (GameMain.NetworkMember != null)
{
CreateMultiplayerCharacterSettings(frame, content);
}
}
private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content)
{
if (skillLayout is null) { return; }
GUIFrame characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null) { Visible = false };
GUILayoutGroup characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform));
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null);
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
{
IgnoreLayoutGroups = false,
TextBlock =
{
AutoScaleHorizontal = true
}
};
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{
newCharacterBox.Text = TextManager.Get("settings");
if (TabMenu.PendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
}
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
{
OnClicked = (button, o) =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
characterSettingsFrame!.Visible = false;
content.Visible = true;
return true;
}
};
}
private void CreateStatPanel(GUIComponent parent, CharacterInfo info)
{
Job job = info.Job;
GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform, Anchor.Center), isHorizontal: true);
new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
{
float posY = component.Rect.Center.Y - component.Rect.Width / 2;
info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false);
});
GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform))
{
AbsoluteSpacing = GUI.IntScale(5),
CanBeFocused = true
};
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
if (!info.OmitJobInMenus)
{
nameBlock.TextColor = job.Prefab.UIColor;
GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor };
}
if (info.PersonalityTrait != null)
{
LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName);
Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString);
GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont);
traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint();
}
ImmutableHashSet<TalentPrefab?> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet();
if (talentsOutsideTree.Any())
{
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null);
GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter);
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont);
talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y);
var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.8f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true)
{
AutoHideScrollBar = false,
ResizeContentToMakeSpaceForScrollBar = false
};
extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter);
extraTalentList.RectTransform.MinSize = new Point(0, GUI.IntScale(65));
extraTalentLayout.Recalculate();
extraTalentList.ForceLayoutRecalculation();
foreach (var extraTalent in talentsOutsideTree)
{
if (extraTalent is null) { continue; }
GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description),
Color = GUIStyle.Green
};
}
}
skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight)
{
AbsoluteSpacing = GUI.IntScale(5),
Stretch = true
};
GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont);
skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null);
TabMenu.CreateSkillList(info.Character, info, skillListBox);
}
private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree)
{
GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter));
selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
List<GUITextBlock> subTreeNames = new List<GUITextBlock>();
foreach (var subTree in tree.TalentSubTrees)
{
GUIListBox talentList;
GUIComponent talentParent;
Vector2 treeSize;
switch (subTree.Type)
{
case TalentTreeType.Primary:
talentList = mainList;
treeSize = new Vector2(1f, 0.5f);
break;
case TalentTreeType.Specialization:
talentList = GetSpecializationList();
treeSize = new Vector2(0.333f, 1f);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\"");
}
talentParent = talentList.Content;
GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(treeSize, talentParent.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter)
{
Stretch = true
};
if (subTree.Type != TalentTreeType.Primary)
{
GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter)
{ MinSize = new Point(0, GUI.IntScale(30)) }, style: null);
subtreeTitleFrame.RectTransform.IsFixedSize = true;
int elementPadding = GUI.IntScale(8);
Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize;
GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader");
subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center));
}
int optionAmount = subTree.TalentOptionStages.Length;
for (int i = 0; i < optionAmount; i++)
{
TalentOption option = subTree.TalentOptionStages[i];
CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info);
}
subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width,
subTreeLayoutGroup.Children.Sum(c => c.Rect.Height + subTreeLayoutGroup.AbsoluteSpacing)));
subTreeLayoutGroup.RectTransform.MinSize = new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Rect.Height);
subTreeLayoutGroup.Recalculate();
if (subTree.Type == TalentTreeType.Specialization)
{
talentList.RectTransform.Resize(new Point(talentList.Rect.Width, Math.Max(subTreeLayoutGroup.Rect.Height, talentList.Rect.Height)));
talentList.RectTransform.MinSize = new Point(0, talentList.Rect.Height);
}
}
var specializationList = GetSpecializationList();
GetSpecializationList().RectTransform.Resize(new Point(specializationList.Content.Children.Sum(c => c.Rect.Width), specializationList.Rect.Height));
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
GUIListBox GetSpecializationList()
{
if (mainList.Content.Children.LastOrDefault() is GUIListBox specializationList)
{
return specializationList;
}
GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null);
return newSpecializationList;
}
}
private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info)
{
int elementPadding = GUI.IntScale(8);
GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter)
{ MinSize = new Point(0, GUI.IntScale(65)) }, style: null);
Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center),
style: "TalentBackground")
{
Color = talentStageStyles[Locked].Color
};
GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false };
GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null)
{
CanBeFocused = false,
Color = talentStageStyles[Locked].Color
};
Point iconSize = cornerIcon.RectTransform.NonScaledSize;
cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2);
GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 0.9f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
HashSet<Identifier> talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(static t => t).ToHashSet();
bool hasShowcase = talentOption.ShowcaseTalent.TryUnwrap(out Identifier showcaseTalentIdentifier);
GUILayoutGroup showcaseLayout = talentOptionLayoutGroup;
if (hasShowcase)
{
talentOptionIdentifiers.Add(showcaseTalentIdentifier);
Point parentSize = talentBackground.RectTransform.NonScaledSize;
GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talentOptionIdentifiers.Count - 1)), parentSize.Y)), style: "GUITooltip")
{
UserData = showcaseTalentIdentifier,
IgnoreLayoutGroups = true,
Visible = false
};
GUILayoutGroup showcaseCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.7f), showCaseFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true };
showCaseTalentFrames.Add(showCaseFrame);
}
foreach (Identifier talentId in talentOptionIdentifiers)
{
if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; }
bool isShowCaseTalent = hasShowcase && talentId == showcaseTalentIdentifier;
GUIComponent talentParent;
if (hasShowcase && talentId != showcaseTalentIdentifier)
{
talentParent = showcaseLayout;
}
else
{
talentParent = talentOptionLayoutGroup;
}
GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentParent.RectTransform), style: null)
{
CanBeFocused = false
};
GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null);
GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + talent.Description),
UserData = talent.Identifier,
PressedColor = pressedColor,
Enabled = info.Character != null,
OnClicked = (button, userData) =>
{
if (isShowCaseTalent)
{
foreach (GUIComponent component in showCaseTalentFrames)
{
if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId)
{
component.RectTransform.ScreenSpaceOffset = new Point((int)(button.Rect.Location.X - component.Rect.Width / 2f + button.Rect.Width / 2f), button.Rect.Location.Y - component.Rect.Height);
component.Visible = true;
}
else
{
component.Visible = false;
}
}
return true;
}
Character? controlledCharacter = info.Character;
if (controlledCharacter is null) { return false; }
if (talentOption.MaxChosenTalents is 1)
{
// deselect other buttons in tier by removing their selected talents from pool
foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren<GUIButton>())
{
if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button)
{
if (!controlledCharacter.HasTalent(otherTalentIdentifier))
{
selectedTalents.Remove(otherTalentIdentifier);
}
}
}
}
Identifier talentIdentifier = (Identifier)userData;
if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
{
if (!selectedTalents.Contains(talentIdentifier))
{
selectedTalents.Add(talentIdentifier);
}
else
{
selectedTalents.Remove(talentIdentifier);
}
}
else if (!controlledCharacter.HasTalent(talentIdentifier))
{
selectedTalents.Remove(talentIdentifier);
}
UpdateTalentInfo();
return true;
},
};
talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
GUIComponent iconImage;
if (talent.Icon is null)
{
iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null)
{
OutlineColor = GUIStyle.Red,
TextColor = GUIStyle.Red,
PressedColor = unselectableColor,
DisabledColor = unselectableColor,
CanBeFocused = false,
};
}
else
{
iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true)
{
Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White,
PressedColor = unselectableColor,
DisabledColor = unselectableColor * 0.5f,
CanBeFocused = false,
};
}
iconImage.Enabled = talentButton.Enabled;
if (isShowCaseTalent) { continue; }
talentButtons.Add(new TalentButton(talentButton, iconImage, talent));
}
talentCornerIcons.Add(new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight));
}
private void CreateFooter(GUIComponent parent, CharacterInfo info)
{
GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), parent.RectTransform, Anchor.TopCenter), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true
};
GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform));
GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null);
experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft),
barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
{
IsHorizontal = true,
};
experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
Shadow = true,
ToolTip = TextManager.Get("experiencetooltip")
};
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight)
{ AutoScaleVertical = true };
talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale")
{
OnClicked = ResetTalentSelection
};
talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale")
{
OnClicked = ApplyTalentSelection,
};
GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
}
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
UpdateTalentInfo();
return true;
}
private void ApplyTalents(Character controlledCharacter)
{
foreach (Identifier talent in CheckTalentSelection(controlledCharacter, selectedTalents))
{
controlledCharacter.GiveTalent(talent);
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData());
}
}
UpdateTalentInfo();
}
private bool ApplyTalentSelection(GUIButton guiButton, object userData)
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter is null) { return false; }
ApplyTalents(controlledCharacter);
return true;
}
public void UpdateTalentInfo()
{
if (!(Character.Controlled is { Info: var info } character)) { return; }
bool unlockedAllTalents = character.HasUnlockedAllTalents();
if (experienceBar is null || experienceText is null) { return; }
if (unlockedAllTalents)
{
experienceText.Text = string.Empty;
experienceBar.BarSize = 1f;
}
else
{
experienceText.Text = $"{info.ExperiencePoints - info.GetExperienceRequiredForCurrentLevel()} / {info.GetExperienceRequiredToLevelUp() - info.GetExperienceRequiredForCurrentLevel()}";
experienceBar.BarSize = info.GetProgressTowardsNextLevel();
}
selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet();
string pointsLeft = info.GetAvailableTalentPoints().ToString();
int talentCount = selectedTalents.Count - info.GetUnlockedTalentsInTree().Count();
if (unlockedAllTalents)
{
talentPointText?.SetRichText($"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖");
}
else if (talentCount > 0)
{
string pointsUsed = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{-talentCount}‖color:end‖";
LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed));
talentPointText?.SetRichText(localizedString);
}
else
{
talentPointText?.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft));
}
foreach (TalentCornerIcon cornerIcon in talentCornerIcons)
{
TalentTreeStageState state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents);
TalentTreeStyle style = talentStageStyles[state];
GUIComponentStyle newStyle = style.ComponentStyle;
cornerIcon.IconComponent.ApplyStyle(newStyle);
cornerIcon.IconComponent.Color = newStyle.Color;
cornerIcon.BackgroundComponent.Color = style.Color;
cornerIcon.GlowComponent.Visible = state == Highlighted;
}
foreach (TalentButton talentButton in talentButtons)
{
Identifier talentIdentifier = talentButton.Identifier;
bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier);
Color newTalentColor = unselectable ? unselectableColor : unselectedColor;
Color hoverColor = Color.White;
bool selected = false;
if (character.HasTalent(talentIdentifier))
{
selected = true;
newTalentColor = GUIStyle.Green;
}
else if (selectedTalents.Contains(talentIdentifier))
{
selected = true;
newTalentColor = GUIStyle.Orange;
hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f);
}
bool shouldOverride = !unselectable || selected;
if (shouldOverride && talentButton.Prefab.ColorOverride.TryUnwrap(out Color overrideColor))
{
newTalentColor = overrideColor;
}
talentButton.IconComponent.Color = newTalentColor;
talentButton.IconComponent.HoverColor = hoverColor;
}
if (skillListBox is null) { return; }
TabMenu.CreateSkillList(character, info, skillListBox);
}
public void AddToGUIUpdateList()
{
bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0;
bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.InfoTab].IsHit();
foreach (GUIComponent component in showCaseTalentFrames)
{
component.AddToGUIUpdateList(order: 1);
if (!component.Visible) { continue; }
if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition)))
{
component.Visible = false;
}
}
}
public void Update()
{
if (Character.Controlled?.Info is not { } characterInfo || talentResetButton is null || talentApplyButton is null) { return; }
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
talentApplyButton.Flash(GUIStyle.Orange);
}
}
}
}
@@ -433,8 +433,8 @@ namespace Barotrauma
Location location = Campaign.Map.CurrentLocation; Location location = Campaign.Map.CurrentLocation;
int hullRepairCost = Campaign.GetHullRepairCost(); int hullRepairCost = CampaignMode.GetHullRepairCost();
int itemRepairCost = Campaign.GetItemRepairCost(); int itemRepairCost = CampaignMode.GetItemRepairCost();
int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost; int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost;
if (location != null) if (location != null)
{ {
@@ -847,7 +847,7 @@ namespace Barotrauma
foreach (UpgradePrefab prefab in prefabs) foreach (UpgradePrefab prefab in prefabs)
{ {
if (prefab.MaxLevel is 0) { continue; } if (prefab.GetMaxLevelForCurrentSub() == 0) { continue; }
CreateUpgradeEntry(prefab, category, parent.Content, submarine, entitiesOnSub); CreateUpgradeEntry(prefab, category, parent.Content, submarine, entitiesOnSub);
} }
} }
@@ -1080,7 +1080,7 @@ namespace Barotrauma
public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true)
{ {
int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category));
} }
@@ -1177,11 +1177,12 @@ namespace Barotrauma
private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel) private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel)
{ {
float nextIncrease = upgradePrefab.IncreaseOnTooltip * (Math.Min(currentLevel + 1, upgradePrefab.MaxLevel)); int maxLevel = upgradePrefab.GetMaxLevelForCurrentSub();
float nextIncrease = upgradePrefab.IncreaseOnTooltip * Math.Min(currentLevel + 1, maxLevel);
if (nextIncrease != 0f) if (nextIncrease != 0f)
{ {
text.Text = $"{Math.Round(nextIncrease, 1)} %"; text.Text = $"{Math.Round(nextIncrease, 1)} %";
if (currentLevel == upgradePrefab.MaxLevel) if (currentLevel == maxLevel)
{ {
text.TextColor = Color.Gray; text.TextColor = Color.Gray;
} }
@@ -1221,7 +1222,7 @@ namespace Barotrauma
{ {
LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody",
("[upgradename]", prefab.Name), ("[upgradename]", prefab.Name),
("[amount]", prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString()));
currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () =>
{ {
if (GameMain.NetworkMember != null) if (GameMain.NetworkMember != null)
@@ -1617,14 +1618,15 @@ namespace Barotrauma
{ {
int currentLevel = campaign.UpgradeManager.GetUpgradeLevel(prefab, category); int currentLevel = campaign.UpgradeManager.GetUpgradeLevel(prefab, category);
LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", prefab.MaxLevel.ToString())); int maxLevel = prefab.GetMaxLevelForCurrentSub();
LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", maxLevel.ToString()));
if (prefabFrame.FindChild("progressbar", true) is { } progressParent) if (prefabFrame.FindChild("progressbar", true) is { } progressParent)
{ {
GUIProgressBar bar = progressParent.GetChild<GUIProgressBar>(); GUIProgressBar bar = progressParent.GetChild<GUIProgressBar>();
if (bar != null) if (bar != null)
{ {
bar.BarSize = currentLevel / (float) prefab.MaxLevel; bar.BarSize = currentLevel / (float)maxLevel;
bar.Color = currentLevel >= prefab.MaxLevel ? GUIStyle.Green : GUIStyle.Orange; bar.Color = currentLevel >= maxLevel ? GUIStyle.Green : GUIStyle.Orange;
} }
GUITextBlock block = progressParent.GetChild<GUITextBlock>(); GUITextBlock block = progressParent.GetChild<GUITextBlock>();
@@ -1637,12 +1639,12 @@ namespace Barotrauma
GUITextBlock priceLabel = textBlocks[0]; GUITextBlock priceLabel = textBlocks[0];
priceLabel.Visible = true; priceLabel.Visible = true;
int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
if (priceLabel != null && !WaitForServerUpdate) if (priceLabel != null && !WaitForServerUpdate)
{ {
priceLabel.Text = TextManager.FormatCurrency(price); priceLabel.Text = TextManager.FormatCurrency(price);
if (currentLevel >= prefab.MaxLevel) if (currentLevel >= maxLevel)
{ {
priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade");
} }
@@ -1651,7 +1653,7 @@ namespace Barotrauma
GUIButton button = buttonParent.GetChild<GUIButton>(); GUIButton button = buttonParent.GetChild<GUIButton>();
if (button != null) if (button != null)
{ {
button.Enabled = currentLevel < prefab.MaxLevel; button.Enabled = currentLevel < maxLevel;
if (WaitForServerUpdate || campaign.GetBalance() < price) if (WaitForServerUpdate || campaign.GetBalance() < price)
{ {
button.Enabled = false; button.Enabled = false;
@@ -1697,13 +1699,14 @@ namespace Barotrauma
foreach (GUIComponent component in indicators.Children) foreach (GUIComponent component in indicators.Children)
{ {
if (!(component is GUIImage image)) { continue; } if (component is not GUIImage image) { continue; }
foreach (UpgradePrefab prefab in prefabs) foreach (UpgradePrefab prefab in prefabs)
{ {
if (component.UserData != prefab) { continue; } if (component.UserData != prefab) { continue; }
if (prefab.MaxLevel is 0) int maxLevel = prefab.GetMaxLevelForCurrentSub();
if (maxLevel == 0)
{ {
component.Visible = false; component.Visible = false;
continue; continue;
@@ -1715,7 +1718,6 @@ namespace Barotrauma
GUIComponentStyle onStyle = styles["upgradeindicatoron".ToIdentifier()]; GUIComponentStyle onStyle = styles["upgradeindicatoron".ToIdentifier()];
GUIComponentStyle dimStyle = styles["upgradeindicatordim".ToIdentifier()]; GUIComponentStyle dimStyle = styles["upgradeindicatordim".ToIdentifier()];
GUIComponentStyle offStyle = styles["upgradeindicatoroff".ToIdentifier()]; GUIComponentStyle offStyle = styles["upgradeindicatoroff".ToIdentifier()];
int maxLevel = prefab.MaxLevel;
if (maxLevel == 0) if (maxLevel == 0)
{ {
@@ -30,7 +30,7 @@ namespace Barotrauma
Data = data, Data = data,
OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) => OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) =>
{ {
GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/"); GameMain.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/");
} }
}); });
} }
@@ -791,6 +791,10 @@ namespace Barotrauma
{ {
GUI.TogglePauseMenu(); GUI.TogglePauseMenu();
} }
else if (GameSession?.Campaign is { ShowCampaignUI: true, ForceMapUI: false })
{
GameSession.Campaign.ShowCampaignUI = false;
}
//open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC //open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC
else if ((Character.Controlled == null || !itemHudActive()) else if ((Character.Controlled == null || !itemHudActive())
&& CharacterHealth.OpenHealthWindow == null && CharacterHealth.OpenHealthWindow == null
@@ -1200,7 +1204,7 @@ namespace Barotrauma
base.OnExiting(sender, args); base.OnExiting(sender, args);
} }
public void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null) public static void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null)
{ {
if (string.IsNullOrEmpty(url)) { return; } if (string.IsNullOrEmpty(url)) { return; }
if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; }
@@ -1218,7 +1222,14 @@ namespace Barotrauma
}; };
msgBox.Buttons[0].OnClicked = (btn, userdata) => msgBox.Buttons[0].OnClicked = (btn, userdata) =>
{ {
ToolBox.OpenFileWithShell(url); try
{
ToolBox.OpenFileWithShell(url);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to open the url {url}", e);
}
msgBox.Close(); msgBox.Close();
return true; return true;
}; };
@@ -86,10 +86,12 @@ namespace Barotrauma
} }
} }
private static bool IsOwner(Client client) => client != null && client.IsOwner;
/// <summary> /// <summary>
/// There is a server-side implementation of the method in <see cref="MultiPlayerCampaign"/> /// There is a server-side implementation of the method in <see cref="MultiPlayerCampaign"/>
/// </summary> /// </summary>
public bool AllowedToManageCampaign(ClientPermissions permissions) public static bool AllowedToManageCampaign(ClientPermissions permissions)
{ {
//allow managing the round if the client has permissions, is the owner, the only client in the server, //allow managing the round if the client has permissions, is the owner, the only client in the server,
//or if no-one has management permissions //or if no-one has management permissions
@@ -97,9 +99,8 @@ namespace Barotrauma
return return
GameMain.Client.HasPermission(permissions) || GameMain.Client.HasPermission(permissions) ||
GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Client.ConnectedClients.Count == 1 ||
GameMain.Client.IsServerOwner || GameMain.Client.IsServerOwner ||
GameMain.Client.ConnectedClients.None(c => c.InGame && (c.IsOwner || c.HasPermission(permissions))); AnyOneAllowedToManageCampaign(permissions);
} }
public static bool AllowedToManageWallets() public static bool AllowedToManageWallets()
@@ -407,6 +407,11 @@ namespace Barotrauma
GUI.SetSavingIndicatorState(success); GUI.SetSavingIndicatorState(success);
crewDead = false; crewDead = false;
if (success)
{
// Event history must be registered before ending the round or it will be cleared
GameMain.GameSession.EventManager.RegisterEventHistory();
}
GameMain.GameSession.EndRound("", traitorResults, transitionType); GameMain.GameSession.EndRound("", traitorResults, transitionType);
var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton;
RoundSummary roundSummary = null; RoundSummary roundSummary = null;
@@ -466,7 +471,6 @@ namespace Barotrauma
if (success) if (success)
{ {
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
GameMain.GameSession.EventManager.RegisterEventHistory();
SaveUtil.SaveGame(GameMain.GameSession.SavePath); SaveUtil.SaveGame(GameMain.GameSession.SavePath);
} }
else else
@@ -506,7 +506,7 @@ namespace Barotrauma
private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType)
{ {
string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name; string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name;
string textTag; string textTag;
if (gameOver) if (gameOver)
@@ -774,7 +774,6 @@ namespace Barotrauma
} }
else else
{ {
bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any);
var selectedContainer = character.SelectedItem?.GetComponent<ItemContainer>(); var selectedContainer = character.SelectedItem?.GetComponent<ItemContainer>();
if (selectedContainer != null && if (selectedContainer != null &&
@@ -802,8 +801,7 @@ namespace Barotrauma
} }
else if (character.HeldItems.Any(i => else if (character.HeldItems.Any(i =>
i.OwnInventory != null && i.OwnInventory != null &&
/*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/ (i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))))
((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))))
{ {
return QuickUseAction.PutToEquippedItem; return QuickUseAction.PutToEquippedItem;
} }
@@ -25,14 +25,31 @@ namespace Barotrauma.Items.Components
foreach (Node node in nodes) foreach (Node node in nodes)
{ {
GameMain.ParticleManager.CreateParticle("swirlysmoke", node.WorldPosition, Vector2.Zero); GameMain.ParticleManager.CreateParticle("swirlysmoke", node.WorldPosition, Vector2.Zero);
if (node.ParentIndex > -1)
{
Vector2 diff = nodes[node.ParentIndex].WorldPosition - node.WorldPosition;
float dist = diff.Length();
Vector2 normalizedDiff = diff / dist;
for (float x = 0.0f; x < dist; x += 50.0f)
{
var spark = GameMain.ParticleManager.CreateParticle("ElectricShock", node.WorldPosition + normalizedDiff * x, Vector2.Zero);
if (spark != null)
{
spark.Size *= 0.3f;
}
}
}
} }
} }
public void DrawElectricity(SpriteBatch spriteBatch) public void DrawElectricity(SpriteBatch spriteBatch)
{ {
if (timer <= 0.0f) { return; }
for (int i = 0; i < nodes.Count; i++) for (int i = 0; i < nodes.Count; i++)
{ {
if (nodes[i].Length <= 1.0f) continue; if (nodes[i].Length <= 1.0f) { continue; }
var node = nodes[i]; var node = nodes[i];
electricitySprite.Draw(spriteBatch, electricitySprite.Draw(spriteBatch,
(i + frameOffset) % electricitySprite.FrameCount, (i + frameOffset) % electricitySprite.FrameCount,
@@ -46,10 +63,16 @@ namespace Barotrauma.Items.Components
if (GameMain.DebugDraw) if (GameMain.DebugDraw)
{ {
for (int i = 0; i < nodes.Count; i++) for (int i = 1; i < nodes.Count; i++)
{ {
if (nodes[i].Length <= 1.0f) continue; GUI.DrawLine(spriteBatch,
GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 5, Color.LightCyan, isFilled: true); new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y),
new Vector2(nodes[nodes[i].ParentIndex].WorldPosition.X, -nodes[nodes[i].ParentIndex].WorldPosition.Y),
Color.LightCyan,
width: 3);
if (nodes[i].Length <= 1.0f) { continue; }
GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 10, Color.LightCyan, isFilled: true);
} }
} }
} }
@@ -265,6 +265,8 @@ namespace Barotrauma.Items.Components
foreach (DeconstructItem deconstructItem in it.Prefab.DeconstructItems) foreach (DeconstructItem deconstructItem in it.Prefab.DeconstructItems)
{ {
if (!deconstructItem.IsValidDeconstructor(item)) { continue; } if (!deconstructItem.IsValidDeconstructor(item)) { continue; }
float percentageHealth = it.Condition / it.MaxCondition;
if (percentageHealth < deconstructItem.MinCondition || percentageHealth > deconstructItem.MaxCondition) { continue; }
RegisterItem(deconstructItem.ItemIdentifier, deconstructItem.Amount); RegisterItem(deconstructItem.ItemIdentifier, deconstructItem.Amount);
} }
@@ -1119,7 +1119,7 @@ namespace Barotrauma.Items.Components
if (it.GetComponent<PowerContainer>() is { } battery) if (it.GetComponent<PowerContainer>() is { } battery)
{ {
int batteryCapacity = (int)(battery.Charge / battery.Capacity * 100f); int batteryCapacity = (int)(battery.Charge / battery.GetCapacity() * 100f);
line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString()); line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString());
} }
else if (it.GetComponent<PowerTransfer>() is { } powerTransfer) else if (it.GetComponent<PowerTransfer>() is { } powerTransfer)
@@ -1028,7 +1028,7 @@ namespace Barotrauma.Items.Components
{ {
foreach (var c in MineralClusters) foreach (var c in MineralClusters)
{ {
var unobtainedMinerals = c.resources.Where(i => i != null && i.GetRootInventoryOwner() == i); var unobtainedMinerals = c.resources.Where(i => i != null && i.GetComponent<Holdable>() is { Attached: true });
if (unobtainedMinerals.None()) { continue; } if (unobtainedMinerals.None()) { continue; }
if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; } if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; }
var i = unobtainedMinerals.FirstOrDefault(); var i = unobtainedMinerals.FirstOrDefault();
@@ -390,7 +390,7 @@ namespace Barotrauma.Items.Components
!ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) !ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false))
{ {
// Docking to an outpost // Docking to an outpost
var subsToLeaveBehind = campaign.GetSubsToLeaveBehind(Item.Submarine); var subsToLeaveBehind = CampaignMode.GetSubsToLeaveBehind(Item.Submarine);
if (subsToLeaveBehind.Any()) if (subsToLeaveBehind.Any())
{ {
enterOutpostPrompt = new GUIMessageBox( enterOutpostPrompt = new GUIMessageBox(
@@ -133,35 +133,43 @@ namespace Barotrauma.Items.Components
public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1)
{ {
if (indicatorSize.X <= 1.0f || indicatorSize.Y <= 1.0f) { return; } Vector2 scaledIndicatorSize = indicatorSize * item.Scale;
if (scaledIndicatorSize.X <= 2.0f || scaledIndicatorSize.Y <= 2.0f) { return; }
const float outlineThickness = 1.0f;
Vector2 itemSize = new Vector2(item.Sprite.SourceRect.Width, item.Sprite.SourceRect.Height) * item.Scale; Vector2 itemSize = new Vector2(item.Sprite.SourceRect.Width, item.Sprite.SourceRect.Height) * item.Scale;
Vector2 indicatorPos = -itemSize / 2 + indicatorPosition * item.Scale; Vector2 indicatorPos = -itemSize / 2.0f + indicatorPosition * item.Scale;
if (item.FlippedX && item.Prefab.CanSpriteFlipX) { indicatorPos.X = -indicatorPos.X - indicatorSize.X * item.Scale; } Vector2 itemPosition = new Vector2(item.DrawPosition.X, -item.DrawPosition.Y);
if (item.FlippedY && item.Prefab.CanSpriteFlipY) { indicatorPos.Y = -indicatorPos.Y - indicatorSize.Y * item.Scale; } Vector2 flip = new Vector2(item.FlippedX && item.Prefab.CanSpriteFlipX ? -1.0f : 1.0f, item.FlippedY && item.Prefab.CanSpriteFlipY ? -1.0f : 1.0f);
Matrix rotate = Matrix.CreateRotationZ(item.RotationRad);
Vector2 center = Vector2.Transform((indicatorPos + (scaledIndicatorSize * 0.5f)) * flip, rotate) + itemPosition;
if (charge > 0 && capacity > 0) if (charge > 0 && capacity > 0)
{ {
float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f);
Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green);
if (!isHorizontal) Vector2 indicatorCenter = (indicatorPos + (scaledIndicatorSize * 0.5f)) * flip;
Vector2 indicatorSize;
if (isHorizontal)
{ {
GUI.DrawRectangle(spriteBatch, float indicatorLength = (scaledIndicatorSize.X - outlineThickness * 2.0f) * chargeRatio;
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - chargeRatio))) + indicatorPos, indicatorCenter.X += -scaledIndicatorSize.X * 0.5f + (flipIndicator ? scaledIndicatorSize.X - outlineThickness - indicatorLength * 0.5f : outlineThickness + indicatorLength * 0.5f);
new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * chargeRatio), indicatorColor, true, indicatorSize = new Vector2(indicatorLength, scaledIndicatorSize.Y);
depth: item.SpriteDepth - 0.00001f);
} }
else else
{ {
GUI.DrawRectangle(spriteBatch, float indicatorLength = (scaledIndicatorSize.Y - outlineThickness * 2.0f) * chargeRatio;
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, indicatorCenter.Y += -scaledIndicatorSize.Y * 0.5f + (flipIndicator ? outlineThickness + indicatorLength * 0.5f : scaledIndicatorSize.Y - outlineThickness - indicatorLength * 0.5f);
new Vector2((indicatorSize.X * item.Scale) * chargeRatio, indicatorSize.Y * item.Scale), indicatorColor, true, indicatorSize = new Vector2(scaledIndicatorSize.X, indicatorLength);
depth: item.SpriteDepth - 0.00001f);
} }
indicatorCenter = Vector2.Transform(indicatorCenter, rotate) + itemPosition;
GUI.DrawFilledRectangle(spriteBatch, indicatorCenter, indicatorSize, indicatorSize * 0.5f, item.RotationRad, indicatorColor, item.SpriteDepth - 0.00001f);
} }
GUI.DrawRectangle(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, GUI.DrawRectangle(spriteBatch, center, scaledIndicatorSize, scaledIndicatorSize * 0.5f, item.RotationRad, Color.Black, item.SpriteDepth - 0.000015f, outlineThickness, GUI.OutlinePosition.Inside);
indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f);
} }
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData) public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData)
@@ -581,7 +581,7 @@ namespace Barotrauma.Items.Components
var battery = recipient.Item?.GetComponent<PowerContainer>(); var battery = recipient.Item?.GetComponent<PowerContainer>();
if (battery == null || battery.Item.Condition <= 0.0f) { continue; } if (battery == null || battery.Item.Condition <= 0.0f) { continue; }
availableCharge += battery.Charge; availableCharge += battery.Charge;
availableCapacity += battery.Capacity; availableCapacity += battery.GetCapacity();
} }
} }
@@ -1415,6 +1415,15 @@ namespace Barotrauma
case EventType.ChangeProperty: case EventType.ChangeProperty:
ReadPropertyChange(msg, false); ReadPropertyChange(msg, false);
break; break;
case EventType.ItemStat:
byte length = msg.ReadByte();
for (int i = 0; i < length; i++)
{
var statIdentifier = INetSerializableStruct.Read<ItemStatManager.TalentStatIdentifier>(msg);
var statValue = msg.ReadSingle();
StatManager.ApplyStat(statIdentifier, statValue);
}
break;
case EventType.Upgrade: case EventType.Upgrade:
Identifier identifier = msg.ReadIdentifier(); Identifier identifier = msg.ReadIdentifier();
byte level = msg.ReadByte(); byte level = msg.ReadByte();
@@ -208,6 +208,13 @@ namespace Barotrauma
DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
} }
public bool CanCharacterBuy()
{
if (!DefaultPrice.RequiresUnlock) { return true; }
return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this);
}
public override void UpdatePlacing(Camera cam) public override void UpdatePlacing(Camera cam)
{ {
Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub);
@@ -162,23 +162,27 @@ namespace Barotrauma
RemoveFogOfWar(StartLocation); RemoveFogOfWar(StartLocation);
GenerateLocationConnectionVisuals(); GenerateAllLocationConnectionVisuals();
} }
partial void GenerateLocationConnectionVisuals() partial void GenerateAllLocationConnectionVisuals()
{ {
foreach (LocationConnection connection in Connections) foreach (LocationConnection connection in Connections)
{ {
Vector2 connectionStart = connection.Locations[0].MapPosition; GenerateLocationConnectionVisuals(connection);
Vector2 connectionEnd = connection.Locations[1].MapPosition;
float connectionLength = Vector2.Distance(connectionStart, connectionEnd);
int iterations = Math.Min((int)Math.Sqrt(connectionLength * generationParams.ConnectionIndicatorIterationMultiplier), 5);
connection.CrackSegments.Clear();
connection.CrackSegments.AddRange(MathUtils.GenerateJaggedLine(
connectionStart, connectionEnd,
iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier));
} }
} }
partial void GenerateLocationConnectionVisuals(LocationConnection connection)
{
Vector2 connectionStart = connection.Locations[0].MapPosition;
Vector2 connectionEnd = connection.Locations[1].MapPosition;
float connectionLength = Vector2.Distance(connectionStart, connectionEnd);
int iterations = Math.Min((int)Math.Sqrt(connectionLength * generationParams.ConnectionIndicatorIterationMultiplier), 5);
connection.CrackSegments.Clear();
connection.CrackSegments.AddRange(MathUtils.GenerateJaggedLine(
connectionStart, connectionEnd,
iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier));
}
private void LocationChanged(Location prevLocation, Location newLocation) private void LocationChanged(Location prevLocation, Location newLocation)
{ {
@@ -414,7 +418,7 @@ namespace Barotrauma
new GUIMessageBox(string.Empty, TextManager.Get("LockedPathTooltip")); new GUIMessageBox(string.Empty, TextManager.Get("LockedPathTooltip"));
} }
//clients aren't allowed to select the location without a permission //clients aren't allowed to select the location without a permission
else if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap) ?? false) else if (CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap))
{ {
connectionHighlightState = 0.0f; connectionHighlightState = 0.0f;
SelectedConnection = connection; SelectedConnection = connection;
@@ -277,16 +277,12 @@ namespace Barotrauma.Networking
public void Clear() public void Clear()
{ {
ID = 0;
lastReceivedID = 0; lastReceivedID = 0;
firstNewID = null; firstNewID = null;
events.Clear();
eventLastSent.Clear(); eventLastSent.Clear();
MidRoundSyncingDone = false; MidRoundSyncingDone = false;
ClearSelf();
} }
/// <summary> /// <summary>
@@ -297,6 +293,10 @@ namespace Barotrauma.Networking
{ {
ID = 0; ID = 0;
events.Clear(); events.Clear();
if (thisClient != null)
{
thisClient.LastSentEntityEventID = 0;
}
} }
} }
} }
@@ -14,7 +14,7 @@ namespace Barotrauma.Networking
{ {
static class PingUtils static class PingUtils
{ {
private static readonly Dictionary<IPAddress, int> activePings = new Dictionary<IPAddress, int>(); private static readonly Dictionary<IPEndPoint, int> activePings = new Dictionary<IPEndPoint, int>();
private static bool steamPingInfoReady; private static bool steamPingInfoReady;
@@ -36,9 +36,9 @@ namespace Barotrauma.Networking
switch (serverInfo.Endpoint) switch (serverInfo.Endpoint)
{ {
case LidgrenEndpoint { NetEndpoint: { Address: var address } }: case LidgrenEndpoint { NetEndpoint: var endPoint }:
GetIPAddressPing(serverInfo, address, onPingDiscovered); GetIPAddressPing(serverInfo, endPoint, onPingDiscovered);
break; break;
case SteamP2PEndpoint steamP2PEndpoint: case SteamP2PEndpoint steamP2PEndpoint:
TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})", TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})",
@@ -131,9 +131,9 @@ namespace Barotrauma.Networking
} }
} }
private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action<ServerInfo> onPingDiscovered) private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action<ServerInfo> onPingDiscovered)
{ {
if (IPAddress.IsLoopback(address)) if (IPAddress.IsLoopback(endPoint.Address))
{ {
serverInfo.Ping = Option<int>.Some(0); serverInfo.Ping = Option<int>.Some(0);
onPingDiscovered(serverInfo); onPingDiscovered(serverInfo);
@@ -142,24 +142,24 @@ namespace Barotrauma.Networking
{ {
lock (activePings) lock (activePings)
{ {
if (activePings.ContainsKey(address)) { return; } if (activePings.ContainsKey(endPoint)) { return; }
activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0); activePings.Add(endPoint, activePings.Any() ? activePings.Values.Max() + 1 : 0);
} }
serverInfo.Ping = Option<int>.None(); serverInfo.Ping = Option<int>.None();
TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000), TaskPool.Add($"PingServerAsync ({endPoint})", PingServerAsync(endPoint, 1000),
rtt => rtt =>
{ {
if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); } if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); }
onPingDiscovered(serverInfo); onPingDiscovered(serverInfo);
lock (activePings) lock (activePings)
{ {
activePings.Remove(address); activePings.Remove(endPoint);
} }
}); });
} }
} }
private static async Task<Option<int>> PingServerAsync(IPAddress ipAddress, int timeOut) private static async Task<Option<int>> PingServerAsync(IPEndPoint endPoint, int timeOut)
{ {
await Task.Yield(); await Task.Yield();
bool shouldGo = false; bool shouldGo = false;
@@ -167,21 +167,21 @@ namespace Barotrauma.Networking
{ {
lock (activePings) lock (activePings)
{ {
shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25; shouldGo = activePings.Count(kvp => kvp.Value < activePings[endPoint]) < 25;
} }
await Task.Delay(25); await Task.Delay(25);
} }
if (ipAddress == null) { return Option<int>.None(); } if (endPoint?.Address == null) { return Option<int>.None(); }
//don't attempt to ping if the address is IPv6 and it's not supported //don't attempt to ping if the address is IPv6 and it's not supported
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); } if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
Ping ping = new Ping(); Ping ping = new Ping();
byte[] buffer = new byte[32]; byte[] buffer = new byte[32];
try try
{ {
PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true)); PingReply pingReply = await ping.SendPingAsync(endPoint.Address, timeOut, buffer, new PingOptions(128, true));
return pingReply.Status switch return pingReply.Status switch
{ {
@@ -191,9 +191,9 @@ namespace Barotrauma.Networking
} }
catch (Exception ex) catch (Exception ex)
{ {
GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + endPoint.Address, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
#if DEBUG #if DEBUG
DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); DebugConsole.NewMessage("Failed to ping a server (" + endPoint.Address + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
#endif #endif
return Option<int>.None(); return Option<int>.None();
@@ -702,10 +702,30 @@ namespace Barotrauma.Networking
{ {
Enabled = !GameMain.NetworkMember.GameStarted Enabled = !GameMain.NetworkMember.GameStarted
}; };
var cargoFrame = new GUIListBox(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomRight, Pivot.BottomLeft))
var cargoFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomRight, Pivot.BottomLeft))
{ {
Visible = false Visible = false
}; };
var cargoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), cargoFrame.RectTransform, Anchor.Center))
{
Stretch = true
};
var filterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), cargoContent.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont);
var entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), filterText.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true);
filterText.RectTransform.MinSize = new Point(0, entityFilterBox.RectTransform.MinSize.Y);
var cargoList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), cargoContent.RectTransform));
entityFilterBox.OnTextChanged += (textBox, text) =>
{
foreach (var child in cargoList.Content.Children)
{
if (child.UserData is not ItemPrefab itemPrefab) { continue; }
child.Visible = string.IsNullOrEmpty(text) || itemPrefab.Name.Contains(text, StringComparison.OrdinalIgnoreCase);
}
return true;
};
cargoButton.UserData = cargoFrame; cargoButton.UserData = cargoFrame;
cargoButton.OnClicked = (button, obj) => cargoButton.OnClicked = (button, obj) =>
{ {
@@ -721,7 +741,7 @@ namespace Barotrauma.Networking
GUITextBlock.AutoScaleAndNormalize(buttonHolder.Children.Select(c => ((GUIButton)c).TextBlock)); GUITextBlock.AutoScaleAndNormalize(buttonHolder.Children.Select(c => ((GUIButton)c).TextBlock));
foreach (ItemPrefab ip in ItemPrefab.Prefabs) foreach (ItemPrefab ip in ItemPrefab.Prefabs.OrderBy(ip => ip.Name))
{ {
if (ip.AllowAsExtraCargo.HasValue) if (ip.AllowAsExtraCargo.HasValue)
{ {
@@ -732,10 +752,10 @@ namespace Barotrauma.Networking
if (!ip.CanBeBought) { continue; } if (!ip.CanBeBought) { continue; }
} }
var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoFrame.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true) var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoList.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true)
{ {
Stretch = true, Stretch = true,
UserData = cargoFrame, UserData = ip,
RelativeSpacing = 0.05f RelativeSpacing = 0.05f
}; };
@@ -778,7 +798,7 @@ namespace Barotrauma.Networking
numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0; numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0;
CoroutineManager.Invoke(() => CoroutineManager.Invoke(() =>
{ {
foreach (var child in cargoFrame.Content.GetAllChildren()) foreach (var child in cargoList.Content.GetAllChildren())
{ {
if (child.GetChild<GUINumberInput>() is GUINumberInput otherNumberInput) if (child.GetChild<GUINumberInput>() is GUINumberInput otherNumberInput)
{ {
@@ -82,6 +82,9 @@ namespace Barotrauma.Particles
[Editable, Serialize(false, IsPropertySaveable.Yes)] [Editable, Serialize(false, IsPropertySaveable.Yes)]
public bool CopyEntityAngle { get; set; } public bool CopyEntityAngle { get; set; }
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")]
public bool CopyEntityDir { get; set; }
[Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)]
public Color ColorMultiplier { get; set; } public Color ColorMultiplier { get; set; }
@@ -167,7 +167,7 @@ namespace Barotrauma
foreach (GUITickBox tickBox in missionTickBoxes) foreach (GUITickBox tickBox in missionTickBoxes)
{ {
bool disable = hasMaxMissions && !tickBox.Selected; bool disable = hasMaxMissions && !tickBox.Selected;
tickBox.Enabled = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) && !disable; tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && !disable;
tickBox.Box.DisabledColor = disable ? tickBox.Box.Color * 0.5f : tickBox.Box.Color * 0.8f; tickBox.Box.DisabledColor = disable ? tickBox.Box.Color * 0.5f : tickBox.Box.Color * 0.8f;
foreach (GUIComponent child in tickBox.Parent.Parent.Children) foreach (GUIComponent child in tickBox.Parent.Parent.Children)
{ {
@@ -315,7 +315,7 @@ namespace Barotrauma
if (GUI.MouseOn == tickBox) { return false; } if (GUI.MouseOn == tickBox) { return false; }
if (tickBox != null) if (tickBox != null)
{ {
if (Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) && tickBox.Enabled) if (CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && tickBox.Enabled)
{ {
tickBox.Selected = !tickBox.Selected; tickBox.Selected = !tickBox.Selected;
} }
@@ -356,10 +356,10 @@ namespace Barotrauma
}; };
tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0);
tickBox.RectTransform.IsFixedSize = true; tickBox.RectTransform.IsFixedSize = true;
tickBox.Enabled = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap); tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap);
tickBox.OnSelected += (GUITickBox tb) => tickBox.OnSelected += (GUITickBox tb) =>
{ {
if (!Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; }
if (tb.Selected) if (tb.Selected)
{ {
@@ -379,7 +379,7 @@ namespace Barotrauma
UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation));
if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending &&
Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap))
{ {
GameMain.Client?.SendCampaignState(); GameMain.Client?.SendCampaignState();
} }
@@ -472,7 +472,12 @@ namespace Barotrauma
{ {
TextGetter = () => TextGetter = () =>
{ {
return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.TotalMaxMissionCount}"); int missionCount = 0;
if (GameMain.GameSession != null && Campaign.Map?.CurrentLocation?.SelectedMissions != null)
{
missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m));
}
return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{missionCount}/{Campaign.Settings.TotalMaxMissionCount}");
} }
}; };
@@ -500,7 +505,7 @@ namespace Barotrauma
return true; return true;
}, },
Enabled = true, Enabled = true,
Visible = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) Visible = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap)
}; };
buttonArea.RectTransform.MinSize = new Point(0, StartButton.RectTransform.MinSize.Y); buttonArea.RectTransform.MinSize = new Point(0, StartButton.RectTransform.MinSize.Y);
@@ -351,7 +351,7 @@ namespace Barotrauma.CharacterEditor
{ {
if (string.IsNullOrEmpty(contentPackageNameElement.Text)) if (string.IsNullOrEmpty(contentPackageNameElement.Text))
{ {
contentPackageNameElement.Flash(); contentPackageNameElement.Flash(useRectangleFlash: true);
return false; return false;
} }
if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower()))
@@ -405,7 +405,7 @@ namespace Barotrauma.CharacterEditor
{ {
if (ContentPackage == null) if (ContentPackage == null)
{ {
contentPackageDropDown.Flash(); contentPackageDropDown.Flash(useRectangleFlash: true);
return false; return false;
} }
@@ -417,7 +417,7 @@ namespace Barotrauma.CharacterEditor
if (!File.Exists(evaluatedTexturePath)) if (!File.Exists(evaluatedTexturePath))
{ {
GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red); GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red);
texturePathElement.Flash(GUIStyle.Red); texturePathElement.Flash(useRectangleFlash: true);
return false; return false;
} }
} }
@@ -425,7 +425,7 @@ namespace Barotrauma.CharacterEditor
if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{ {
GUI.AddMessage(TextManager.Get("WrongFileType"), GUIStyle.Red); GUI.AddMessage(TextManager.Get("WrongFileType"), GUIStyle.Red);
texturePathElement.Flash(GUIStyle.Red); texturePathElement.Flash(useRectangleFlash: true);
return false; return false;
} }
if (IsCopy) if (IsCopy)
@@ -486,7 +486,8 @@ namespace Barotrauma.CharacterEditor
{ {
PlaySoundOnSelect = true, PlaySoundOnSelect = true,
}; };
var removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") var limbButtonSize = Vector2.One * 0.8f;
var removeLimbButton = new GUIButton(new RectTransform(limbButtonSize, limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton")
{ {
OnClicked = (b, d) => OnClicked = (b, d) =>
{ {
@@ -497,7 +498,7 @@ namespace Barotrauma.CharacterEditor
return true; return true;
} }
}; };
var addLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton") var addLimbButton = new GUIButton(new RectTransform(limbButtonSize, limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton")
{ {
OnClicked = (b, d) => OnClicked = (b, d) =>
{ {
@@ -16,8 +16,8 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosEnabled = true;
Hull.EditFire = false; Hull.EditFire = false;
Hull.EditWater = false; Hull.EditWater = false;
#endif
HumanAIController.DisableCrewAI = false; HumanAIController.DisableCrewAI = false;
#endif
} }
protected virtual void DeselectEditorSpecific() { } protected virtual void DeselectEditorSpecific() { }
@@ -334,7 +334,7 @@ namespace Barotrauma
OnClicked = (button, userData) => OnClicked = (button, userData) =>
{ {
string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value; string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value;
GameMain.Instance.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); GameMain.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice");
return true; return true;
} }
}; };
@@ -1011,7 +1011,7 @@ namespace Barotrauma
GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f);
if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) if (mouseOn && PlayerInput.PrimaryMouseButtonClicked())
{ {
GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com");
} }
} }
textPos.Y -= textSize.Y; textPos.Y -= textSize.Y;
@@ -1817,7 +1817,11 @@ namespace Barotrauma
subList = dropDown.ListBox.Content; subList = dropDown.ListBox.Content;
} }
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), subList.RectTransform) { MinSize = new Point(0, 25) }, var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), subList.RectTransform)
{
//enough space for 2 lines (price and class) + some padding
MinSize = new Point(0, (int)(GUIStyle.SmallFont.LineHeight * 2.3f))
},
style: "ListBoxElement") style: "ListBoxElement")
{ {
ToolTip = sub.Description, ToolTip = sub.Description,
@@ -7,6 +7,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
@@ -953,8 +955,19 @@ namespace Barotrauma
okButton.Enabled = false; okButton.Enabled = false;
okButton.OnClicked = (btn, userdata) => okButton.OnClicked = (btn, userdata) =>
{ {
if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; } if (Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))
JoinServer(endpoint, ""); {
JoinServer(endpoint, "");
}
else if (LidgrenEndpoint.ParseFromWithHostNameCheck(endpointBox.Text, tryParseHostName: true).TryUnwrap(out var lidgrenEndpoint))
{
JoinServer(lidgrenEndpoint, "");
}
else
{
new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("invalidipaddress", "[serverip]:[port]", endpointBox.Text));
endpointBox.Flash();
}
msgBox.Close(); msgBox.Close();
return false; return false;
}; };
@@ -379,6 +379,9 @@ namespace Barotrauma
void CreateSprite(ContentXElement element) void CreateSprite(ContentXElement element)
{ {
//empty element, probably an item variant?
if (element.Attributes().None()) { return; }
string spriteFolder = ""; string spriteFolder = "";
ContentPath texturePath = null; ContentPath texturePath = null;
@@ -1920,9 +1920,15 @@ namespace Barotrauma
{ {
filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}";
} }
if (!modProject.Files.Any(f => f.Type == subFileType && if (!modProject.Files.Any(f => f.Type == subFileType && f.Path == filePath))
f.Path == filePath))
{ {
//check if there's a file with the same name but different filename case
var matchingFile = modProject.Files.FirstOrDefault(f => f.Type == subFileType && filePath.CleanUpPath().Equals(f.Path.CleanUpPath(), StringComparison.OrdinalIgnoreCase));
if (matchingFile != null)
{
File.Delete(matchingFile.Path.Replace(ContentPath.ModDirStr, packageDir));
modProject.RemoveFile(matchingFile);
}
var newFile = ModProject.File.FromPath(filePath, subFileType); var newFile = ModProject.File.FromPath(filePath, subFileType);
modProject.AddFile(newFile); modProject.AddFile(newFile);
} }
@@ -2479,7 +2485,7 @@ namespace Barotrauma
new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), tierGroup.RectTransform), NumberType.Int) new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), tierGroup.RectTransform), NumberType.Int)
{ {
IntValue = SubmarineInfo.GetDefaultTier(MainSub.Info.Price), IntValue = MainSub.Info.Tier,
MinValueInt = 1, MinValueInt = 1,
MaxValueInt = 3, MaxValueInt = 3,
OnValueChanged = (numberInput) => OnValueChanged = (numberInput) =>
@@ -2821,6 +2827,7 @@ namespace Barotrauma
OnClicked = (button, o) => OnClicked = (button, o) =>
{ {
var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage) var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage)
.Where(cp => cp != null)
.Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet(); .Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet();
var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray(); var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray();
tickboxes.ForEach(tb => tb.Selected = requiredPackages.Contains(tb.UserData as string ?? "")); tickboxes.ForEach(tb => tb.Selected = requiredPackages.Contains(tb.UserData as string ?? ""));
@@ -2919,7 +2926,7 @@ namespace Barotrauma
subTypeDropdown.SelectItem(MainSub.Info.Type); subTypeDropdown.SelectItem(MainSub.Info.Type);
if (quickSave) { SaveSub(null); } if (quickSave) { SaveSub(packageToSaveInList.SelectedData as ContentPackage); }
} }
private void CreateSaveAssemblyScreen() private void CreateSaveAssemblyScreen()
@@ -21,6 +21,7 @@ namespace Barotrauma
public static Character? dummyCharacter; public static Character? dummyCharacter;
public static Effect? BlueprintEffect; public static Effect? BlueprintEffect;
public TabMenu? TabMenu;
public TestScreen() public TestScreen()
{ {
@@ -49,9 +50,10 @@ namespace Barotrauma
} }
dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false);
dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom(Rand.RandSync.Unsynced)); dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "assistant"));
dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Info.Name = "Galldren";
dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Inventory.CreateSlots();
dummyCharacter.Info.GiveExperience(999999);
miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false); miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false);
@@ -61,6 +63,7 @@ namespace Barotrauma
} }
Character.Controlled = dummyCharacter; Character.Controlled = dummyCharacter;
GameMain.World.ProcessChanges(); GameMain.World.ProcessChanges();
TabMenu = new TabMenu();
} }
public override void AddToGUIUpdateList() public override void AddToGUIUpdateList()
@@ -68,35 +71,37 @@ namespace Barotrauma
Frame.AddToGUIUpdateList(); Frame.AddToGUIUpdateList();
CharacterHUD.AddToGUIUpdateList(dummyCharacter); CharacterHUD.AddToGUIUpdateList(dummyCharacter);
dummyCharacter?.SelectedItem?.AddToGUIUpdateList(); dummyCharacter?.SelectedItem?.AddToGUIUpdateList();
TabMenu?.AddToGUIUpdateList();
} }
public override void Update(double deltaTime) public override void Update(double deltaTime)
{ {
base.Update(deltaTime); base.Update(deltaTime);
TabMenu?.Update((float)deltaTime);
if (dummyCharacter is { } dummy && miniMapItem is { } item) // if (dummyCharacter is { } dummy && miniMapItem is { } item)
{ // {
if (dummy.SelectedItem != item) // if (dummy.SelectedConstruction != item)
{ // {
dummy.SelectedItem = item; // dummy.SelectedConstruction = item;
} // }
//
dummy.SelectedItem?.UpdateHUD(Cam, dummy, (float)deltaTime); // dummy.SelectedConstruction?.UpdateHUD(Cam, dummy, (float)deltaTime);
Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); // Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position);
//
foreach (Limb limb in dummy.AnimController.Limbs) // foreach (Limb limb in dummy.AnimController.Limbs)
{ // {
limb.body.SetTransform(pos, 0.0f); // limb.body.SetTransform(pos, 0.0f);
} // }
//
if (dummy.AnimController?.Collider is { } collider) // if (dummy.AnimController?.Collider is { } collider)
{ // {
collider.SetTransform(pos, 0); // collider.SetTransform(pos, 0);
} // }
//
dummy.ControlLocalPlayer((float)deltaTime, Cam, false); // dummy.ControlLocalPlayer((float)deltaTime, Cam, false);
dummy.Control((float)deltaTime, Cam); // dummy.Control((float)deltaTime, Cam);
} // }
} }
public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
@@ -1332,16 +1332,18 @@ namespace Barotrauma
} }
} }
private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) private static void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property)
{ {
if (entity is ItemComponent e) if (GameMain.Client != null)
{ {
entity = e.Item; if (entity is Item item)
} {
GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property, item));
if (GameMain.Client != null && entity is Item item) }
{ else if (entity is ItemComponent ic)
GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property)); {
GameMain.Client.CreateEntityEvent(ic.Item, new Item.ChangePropertyEventData(property, ic));
}
} }
} }
@@ -785,13 +785,7 @@ namespace Barotrauma
{ {
OnClicked = (btn, obj) => OnClicked = (btn, obj) =>
{ {
GameSettings.SetCurrentConfig(unsavedConfig); ApplyInstalledModChanges();
if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu &&
mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods)
{
mutableWorkshopMenu.Apply();
}
GameSettings.SaveCurrentConfig();
mainFrame.Flash(color: GUIStyle.Green); mainFrame.Flash(color: GUIStyle.Green);
return false; return false;
}, },
@@ -804,6 +798,17 @@ namespace Barotrauma
}; };
} }
public void ApplyInstalledModChanges()
{
GameSettings.SetCurrentConfig(unsavedConfig);
if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu &&
mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods)
{
mutableWorkshopMenu.Apply();
}
GameSettings.SaveCurrentConfig();
}
public void Close() public void Close()
{ {
if (GameMain.Client is null || GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) if (GameMain.Client is null || GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled)
@@ -21,6 +21,13 @@ namespace Barotrauma
private Entity soundEmitter; private Entity soundEmitter;
private double loopStartTime; private double loopStartTime;
private bool loopSound; private bool loopSound;
/// <summary>
/// Each new sound overrides the existing sounds that were launched with this status effect, meaning the old sound will be faded out and disposed and the new sound will be played instead of the old.
/// Normally the call to play the sound is ignored if there's an existing sound playing when the effect triggers.
/// Used for example for ensuring that rapid playing sounds restart playing even when the previous clip(s) have not yet stopped.
/// Use with caution.
/// </summary>
private bool forcePlaySounds;
partial void InitProjSpecific(ContentXElement element, string parentDebugName) partial void InitProjSpecific(ContentXElement element, string parentDebugName)
{ {
@@ -50,6 +57,7 @@ namespace Barotrauma
break; break;
} }
} }
forcePlaySounds = element.GetAttributeBool(nameof(forcePlaySounds), false);
} }
partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Hull hull, Vector2 worldPosition, bool playSound) partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Hull hull, Vector2 worldPosition, bool playSound)
@@ -71,7 +79,7 @@ namespace Barotrauma
{ {
angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
particleRotation = -item.body.Rotation; particleRotation = -item.body.Rotation;
if (item.body.Dir < 0.0f) if (emitter.Prefab.Properties.CopyEntityDir && item.body.Dir < 0.0f)
{ {
particleRotation += MathHelper.Pi; particleRotation += MathHelper.Pi;
mirrorAngle = true; mirrorAngle = true;
@@ -96,7 +104,9 @@ namespace Barotrauma
{ {
angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
particleRotation = -targetLimb.body.Rotation; particleRotation = -targetLimb.body.Rotation;
if (targetLimb.body.Dir < 0.0f) float offset = targetLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
particleRotation += offset;
if (emitter.Prefab.Properties.CopyEntityDir && targetLimb.body.Dir < 0.0f)
{ {
particleRotation += MathHelper.Pi; particleRotation += MathHelper.Pi;
mirrorAngle = true; mirrorAngle = true;
@@ -112,10 +122,14 @@ namespace Barotrauma
private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition) private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition)
{ {
if (sounds.Count == 0) return; if (sounds.Count == 0) { return; }
if (soundChannel == null || !soundChannel.IsPlaying) if (soundChannel == null || !soundChannel.IsPlaying || forcePlaySounds)
{ {
if (soundChannel != null && soundChannel.IsPlaying)
{
soundChannel.FadeOutAndDispose();
}
if (soundSelectionMode == SoundSelectionMode.All) if (soundSelectionMode == SoundSelectionMode.All)
{ {
foreach (RoundSound sound in sounds) foreach (RoundSound sound in sounds)
@@ -93,7 +93,7 @@ namespace Barotrauma.Steam
return (left, center, right); return (left, center, right);
} }
private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) private static void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to)
{ {
if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null)
{ {
@@ -197,7 +197,11 @@ namespace Barotrauma.Steam
out onInstalledInfoButtonHit, out var deselect); out onInstalledInfoButtonHit, out var deselect);
GUILayoutGroup mainLayout = GUILayoutGroup mainLayout =
new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter)
{
Stretch = true,
AbsoluteSpacing = GUI.IntScale(5)
};
mainLayout.RectTransform.SetAsFirstChild(); mainLayout.RectTransform.SetAsFirstChild();
var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f);
@@ -257,7 +261,12 @@ namespace Barotrauma.Steam
right.ChildAnchor = Anchor.TopRight; right.ChildAnchor = Anchor.TopRight;
//enabled mods //enabled mods
Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); var label = Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont);
new GUIImage(new RectTransform(new Point(label.Rect.Height), label.RectTransform, Anchor.CenterRight), style: "GUIButtonInfo")
{
ToolTip = TextManager.Get("ModLoadOrderExplanation")
};
var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform))
{ {
CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, CurrentDragMode = GUIListBox.DragMode.DragOutsideBox,
@@ -478,7 +487,7 @@ namespace Barotrauma.Steam
{ {
string str = modsListFilter.Text; string str = modsListFilter.Text;
enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children)
.ForEach(c => c.Visible = !(c.UserData is ContentPackage p) .ForEach(c => c.Visible = c.UserData is not ContentPackage p
|| ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); || ModNameMatches(p, str) && ModMatchesTickboxes(p, c));
} }
@@ -504,12 +513,12 @@ namespace Barotrauma.Steam
//are enabled, and all files match either of them so show this mod //are enabled, and all files match either of them so show this mod
} }
else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected
&& p.Files.Any(f => !(f is BaseSubFile))) && p.Files.Any(f => f is not BaseSubFile))
{ {
matches = false; matches = false;
} }
else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected
&& p.Files.Any(f => !(f is ItemAssemblyFile))) && p.Files.Any(f => f is not ItemAssemblyFile))
{ {
matches = false; matches = false;
} }
@@ -520,7 +529,7 @@ namespace Barotrauma.Steam
private void PrepareToShowModInfo(ContentPackage mod) private void PrepareToShowModInfo(ContentPackage mod)
{ {
if (!mod.UgcId.TryUnwrap(out var ugcId) if (!mod.UgcId.TryUnwrap(out var ugcId)
|| !(ugcId is SteamWorkshopId workshopId)) { return; } || ugcId is not SteamWorkshopId workshopId) { return; }
TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value), TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value),
t => t =>
{ {
@@ -598,13 +598,14 @@ namespace Barotrauma.Steam
bool reinstallAction(GUIButton button, object o) bool reinstallAction(GUIButton button, object o)
{ {
SettingsMenu.Instance?.ApplyInstalledModChanges();
int prevIndex = ContentPackageManager.EnabledPackages.Regular.IndexOf(contentPackage); int prevIndex = ContentPackageManager.EnabledPackages.Regular.IndexOf(contentPackage);
TaskPool.AddIfNotFound($"Reinstall{workshopItem.Id}", TaskPool.AddIfNotFound($"Reinstall{workshopItem.Id}",
SteamManager.Workshop.Reinstall(workshopItem), t => SteamManager.Workshop.Reinstall(workshopItem), t =>
{ {
ContentPackageManager.WorkshopPackages.Refresh(); ContentPackageManager.WorkshopPackages.Refresh();
ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); ContentPackageManager.EnabledPackages.RefreshUpdatedMods();
if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && !mutableWorkshopMenu.ViewingItemDetails)
{ {
mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true); mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true);
} }
@@ -44,12 +44,26 @@ namespace Barotrauma.Steam
public MutableWorkshopMenu(GUIFrame parent) : base(parent) public MutableWorkshopMenu(GUIFrame parent) : base(parent)
{ {
var mainLayout var mainLayout
= new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false); = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false)
{
Stretch = true,
AbsoluteSpacing = GUI.IntScale(4)
};
tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true)
{ Stretch = true }; { Stretch = true };
tabContents = new Dictionary<Tab, (GUIButton Button, GUIFrame Content)>(); tabContents = new Dictionary<Tab, (GUIButton Button, GUIFrame Content)>();
new GUIButton(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform, Anchor.BottomLeft),
style: "GUIButtonSmall", text: TextManager.Get("FindModsButton"))
{
OnClicked = (button, o) =>
{
SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/");
return false;
}
};
contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null);
new GUICustomComponent(new RectTransform(Vector2.Zero, mainLayout.RectTransform), new GUICustomComponent(new RectTransform(Vector2.Zero, mainLayout.RectTransform),
@@ -130,17 +144,8 @@ namespace Barotrauma.Steam
{ {
tabContents[Tab.PopularMods].Button.Enabled = false; tabContents[Tab.PopularMods].Button.Enabled = false;
} }
GUIFrame listFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), content.RectTransform), style: null); GUIFrame listFrame = new GUIFrame(new RectTransform(Vector2.One, content.RectTransform), style: null);
CreateWorkshopItemList(listFrame, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo); CreateWorkshopItemList(listFrame, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo);
new GUIButton(new RectTransform((1.0f, 0.05f), content.RectTransform, Anchor.BottomLeft),
style: "GUIButtonSmall", text: TextManager.Get("FindModsButton"))
{
OnClicked = (button, o) =>
{
SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/");
return false;
}
};
} }
private void CreatePublishTab(out GUIListBox selfModsList) private void CreatePublishTab(out GUIListBox selfModsList)
@@ -543,7 +543,8 @@ namespace Barotrauma.Steam
var localModProject = new ModProject(localPackage) var localModProject = new ModProject(localPackage)
{ {
UgcId = Option<ContentPackageId>.Some(new SteamWorkshopId(resultId)) UgcId = Option<ContentPackageId>.Some(new SteamWorkshopId(resultId)),
ModVersion = modVersion
}; };
localModProject.DiscardHashAndInstallTime(); localModProject.DiscardHashAndInstallTime();
localModProject.Save(localPackage.Path); localModProject.Save(localPackage.Path);
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.19.11.0</Version> <Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.19.11.0</Version> <Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.19.11.0</Version> <Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.19.11.0</Version> <Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.19.11.0</Version> <Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -70,7 +70,7 @@ namespace Barotrauma
msg.WriteByte((byte)Job.Variant); msg.WriteByte((byte)Job.Variant);
foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier)) foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier))
{ {
msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier).Level); msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f);
} }
} }
else else
@@ -219,9 +219,9 @@ namespace Barotrauma
else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500) else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500)
{ {
#if DEBUG || UNSTABLE #if DEBUG || UNSTABLE
DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); DebugConsole.AddWarning($"Large discrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID.");
#endif #endif
LastNetworkUpdateID = networkUpdateID; LastNetworkUpdateID = LastProcessedID = networkUpdateID;
} }
if (memInput.Count > 60) if (memInput.Count > 60)
{ {
@@ -549,7 +549,7 @@ namespace Barotrauma
msg.WriteByte((byte)statType); msg.WriteByte((byte)statType);
foreach (var savedStatValue in Info.SavedStatValues[statType]) foreach (var savedStatValue in Info.SavedStatValues[statType])
{ {
msg.WriteString(savedStatValue.StatIdentifier); msg.WriteIdentifier(savedStatValue.StatIdentifier);
msg.WriteSingle(savedStatValue.StatValue); msg.WriteSingle(savedStatValue.StatValue);
msg.WriteBoolean(savedStatValue.RemoveOnDeath); msg.WriteBoolean(savedStatValue.RemoveOnDeath);
} }
@@ -1408,6 +1408,21 @@ namespace Barotrauma
GameMain.Server.PrintSenderTransters(); GameMain.Server.PrintSenderTransters();
})); }));
AssignOnExecute("resetcharacternetstate", (string[] args) =>
{
if (GameMain.Server == null) { return; }
if (args.Length < 1)
{
ThrowError("Invalid parameters. The command should be formatted as \"resetcharacternetstate [character]\". If the names consist of multiple words, you should surround them with quotation marks.");
return;
}
var character = FindMatchingCharacter(args.Skip(1).ToArray(), false);
character?.ResetNetState();
});
commands.Add(new Command("eventdata", "", (string[] args) => commands.Add(new Command("eventdata", "", (string[] args) =>
{ {
if (args.Length == 0) { return; } if (args.Length == 0) { return; }
@@ -158,7 +158,7 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile);
if (doc?.Root == null) if (doc?.Root == null)
{ {
DebugConsole.ThrowError("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings."); DebugConsole.AddWarning("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings.");
} }
else else
{ {
@@ -16,16 +16,15 @@ namespace Barotrauma
/// <summary> /// <summary>
/// There is a client-side implementation of the method in <see cref="CampaignMode"/> /// There is a client-side implementation of the method in <see cref="CampaignMode"/>
/// </summary> /// </summary>
public bool AllowedToManageCampaign(Client client, ClientPermissions permissions) public static bool AllowedToManageCampaign(Client client, ClientPermissions permissions)
{ {
//allow managing the campaign if the client has permissions, is the owner, or the only client in the server, //allow managing the campaign if the client has permissions, is the owner, or the only client in the server,
//or if no-one has management permissions //or if no-one has management permissions
return return
client.HasPermission(permissions) || client.HasPermission(permissions) ||
client.HasPermission(ClientPermissions.ManageCampaign) || client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Server.ConnectedClients.Count == 1 ||
IsOwner(client) || IsOwner(client) ||
GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions))); AnyOneAllowedToManageCampaign(permissions);
} }
public bool AllowedToManageWallets(Client client) public bool AllowedToManageWallets(Client client)
@@ -347,6 +347,8 @@ namespace Barotrauma
(GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c); (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c);
} }
} }
// Event history must be registered before ending the round or it will be cleared
GameMain.GameSession.EventManager.RegisterEventHistory();
} }
GameMain.GameSession.EndRound("", traitorResults, transitionType); GameMain.GameSession.EndRound("", traitorResults, transitionType);
@@ -360,7 +362,6 @@ namespace Barotrauma
LeaveUnconnectedSubs(leavingSub); LeaveUnconnectedSubs(leavingSub);
NextLevel = newLevel; NextLevel = newLevel;
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
GameMain.GameSession.EventManager.RegisterEventHistory();
SaveUtil.SaveGame(GameMain.GameSession.SavePath); SaveUtil.SaveGame(GameMain.GameSession.SavePath);
} }
else else
@@ -1019,7 +1020,7 @@ namespace Barotrauma
UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); UpgradeManager.PurchaseUpgrade(prefab, category, client: sender);
// unstable logging // unstable logging
int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation);
int level = UpgradeManager.GetUpgradeLevel(prefab, category); int level = UpgradeManager.GetUpgradeLevel(prefab, category);
GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage);
} }
@@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components
{ {
var allowOutpostAutoDocking = (AllowOutpostAutoDocking)msg.ReadByte(); var allowOutpostAutoDocking = (AllowOutpostAutoDocking)msg.ReadByte();
if (outpostAutoDockingPromptShown && if (outpostAutoDockingPromptShown &&
(GameMain.GameSession?.Campaign?.AllowedToManageCampaign(c, ClientPermissions.ManageMap) ?? false)) CampaignMode.AllowedToManageCampaign(c, ClientPermissions.ManageMap))
{ {
this.allowOutpostAutoDocking = allowOutpostAutoDocking; this.allowOutpostAutoDocking = allowOutpostAutoDocking;
} }
@@ -106,6 +106,14 @@ namespace Barotrauma
$"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})"); $"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})");
} }
break; break;
case SetItemStatEventData setItemStatEventData:
msg.WriteByte((byte)setItemStatEventData.Stats.Count);
foreach (var (key, value) in setItemStatEventData.Stats)
{
msg.WriteNetSerializableStruct(key);
msg.WriteSingle(value);
}
break;
case UpgradeEventData upgradeEventData: case UpgradeEventData upgradeEventData:
var upgrade = upgradeEventData.Upgrade; var upgrade = upgradeEventData.Upgrade;
var upgradeTargets = upgrade.TargetComponents; var upgradeTargets = upgrade.TargetComponents;
@@ -152,7 +152,9 @@ namespace Barotrauma.Networking
public bool IsBanned(AccountId accountId, out string reason) public bool IsBanned(AccountId accountId, out string reason)
{ {
RemoveExpired(); RemoveExpired();
var bannedPlayer = bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)); var bannedPlayer =
bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)) ??
bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out Address adr) && adr is SteamP2PAddress steamAdr && steamAdr.SteamId.Equals(accountId));
reason = bannedPlayer?.Reason ?? string.Empty; reason = bannedPlayer?.Reason ?? string.Empty;
return bannedPlayer != null; return bannedPlayer != null;
} }
@@ -40,6 +40,8 @@ namespace Barotrauma.Networking
public float ChatSpamTimer; public float ChatSpamTimer;
public int ChatSpamCount; public int ChatSpamCount;
public string RejectedName;
public int RoundsSincePlayedAsTraitor; public int RoundsSincePlayedAsTraitor;
public float KickAFKTimer; public float KickAFKTimer;
@@ -69,6 +71,9 @@ namespace Barotrauma.Networking
public DateTime JoinTime; public DateTime JoinTime;
public static readonly TimeSpan NameChangeCoolDown = new TimeSpan(hours: 0, minutes: 0, seconds: 30);
public DateTime LastNameChangeTime;
private CharacterInfo characterInfo; private CharacterInfo characterInfo;
public CharacterInfo CharacterInfo public CharacterInfo CharacterInfo
{ {
@@ -486,9 +486,18 @@ namespace Barotrauma.Networking
// -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer // -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer
if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine)) if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine))
{ {
if (ServerSettings.AutoRestart) ServerSettings.AutoRestartTimer = Math.Max(ServerSettings.AutoRestartInterval, 5.0f); if (ServerSettings.AutoRestart) { ServerSettings.AutoRestartTimer = Math.Max(ServerSettings.AutoRestartInterval, 5.0f); }
//GameMain.NetLobbyScreen.StartButtonEnabled = true;
if (startGameCoroutine.Exception != null && OwnerConnection != null)
{
SendConsoleMessage(
startGameCoroutine.Exception.Message + '\n' +
(startGameCoroutine.Exception.StackTrace?.CleanupStackTrace() ?? "null"),
connectedClients.Find(c => c.Connection == OwnerConnection),
Color.Red);
}
EndGame();
GameMain.NetLobbyScreen.LastUpdateID++; GameMain.NetLobbyScreen.LastUpdateID++;
startGameCoroutine = null; startGameCoroutine = null;
@@ -1378,8 +1387,8 @@ namespace Barotrauma.Networking
if (end) if (end)
{ {
if (mpCampaign == null || if (mpCampaign == null ||
mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) ||
mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign)) CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign))
{ {
bool save = inc.ReadBoolean(); bool save = inc.ReadBoolean();
if (GameStarted) if (GameStarted)
@@ -1409,7 +1418,7 @@ namespace Barotrauma.Networking
SendDirectChatMessage("Cannot continue the campaign from the previous save (round already running).", sender, ChatMessageType.Error); SendDirectChatMessage("Cannot continue the campaign from the previous save (round already running).", sender, ChatMessageType.Error);
break; break;
} }
else if (mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)) else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))
{ {
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath);
} }
@@ -1420,7 +1429,7 @@ namespace Barotrauma.Networking
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
StartGame(); StartGame();
} }
else if (mpCampaign != null && (mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)))
{ {
var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
//don't force location if we've teleported //don't force location if we've teleported
@@ -1991,7 +2000,7 @@ namespace Barotrauma.Networking
//and assume the message was received, so we don't have to keep resending //and assume the message was received, so we don't have to keep resending
//these large initial messages until the client acknowledges receiving them //these large initial messages until the client acknowledges receiving them
c.LastRecvLobbyUpdate++; c.LastRecvLobbyUpdate = GameMain.NetLobbyScreen.LastUpdateID;
} }
else else
@@ -2010,7 +2019,7 @@ namespace Barotrauma.Networking
c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID)); c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID));
for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++) for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{ {
if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > MsgConstants.MTU - 5) if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > MsgConstants.MTU - 5 && i > 0)
{ {
//not enough room in this packet //not enough room in this packet
return; return;
@@ -2589,26 +2598,24 @@ namespace Barotrauma.Networking
public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false) public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false)
{ {
if (!GameStarted) if (GameStarted)
{ {
return; if (GameSettings.CurrentConfig.VerboseLogging)
} {
Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage);
if (GameSettings.CurrentConfig.VerboseLogging) }
{ else
Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage); {
Log("Ending the round...", ServerLog.MessageType.ServerMessage);
} }
else
{
Log("Ending the round...", ServerLog.MessageType.ServerMessage);
} }
string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded");
var traitorResults = TraitorManager?.GetEndResults() ?? new List<TraitorMissionResult>(); var traitorResults = TraitorManager?.GetEndResults() ?? new List<TraitorMissionResult>();
List<Mission> missions = GameMain.GameSession.Missions.ToList(); List<Mission> missions = GameMain.GameSession.Missions.ToList();
if (GameMain.GameSession.IsRunning) if (GameMain.GameSession is { IsRunning: true })
{ {
GameMain.GameSession.EndRound(endMessage, traitorResults); GameMain.GameSession.EndRound(endMessage, traitorResults);
} }
@@ -2634,7 +2641,10 @@ namespace Barotrauma.Networking
c.PositionUpdateLastSent.Clear(); c.PositionUpdateLastSent.Clear();
} }
KarmaManager.OnRoundEnded(); if (GameStarted)
{
KarmaManager.OnRoundEnded();
}
RespawnManager = null; RespawnManager = null;
GameStarted = false; GameStarted = false;
@@ -2703,6 +2713,21 @@ namespace Barotrauma.Networking
CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte();
if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; } if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; }
var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime;
if (timeSinceNameChange < Client.NameChangeCoolDown)
{
//only send once per second at most to prevent using this for spamming
if (timeSinceNameChange.TotalSeconds > 1)
{
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
}
c.NameId = nameId;
c.RejectedName = newName;
return false;
}
if (!newJob.IsEmpty) if (!newJob.IsEmpty)
{ {
if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob) if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob)
@@ -2721,26 +2746,25 @@ namespace Barotrauma.Networking
public bool TryChangeClientName(Client c, string newName) public bool TryChangeClientName(Client c, string newName)
{ {
newName = Client.SanitizeName(newName); newName = Client.SanitizeName(newName);
//update client list even if the name cannot be changed to the one sent by the client, if (newName != c.Name && !string.IsNullOrEmpty(newName) && IsNameValid(c, newName))
//so the client will be informed what their actual name is
LastClientListUpdateID++;
if (newName == c.Name || string.IsNullOrEmpty(newName)) { return false; }
if (IsNameValid(c, newName))
{ {
c.LastNameChangeTime = DateTime.Now;
string oldName = c.Name; string oldName = c.Name;
c.Name = newName; c.Name = newName;
c.RejectedName = string.Empty;
SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
LastClientListUpdateID++;
return true; return true;
} }
else else
{ {
//update client list even if the name cannot be changed to the one sent by the client,
//so the client will be informed what their actual name is
LastClientListUpdateID++;
return false; return false;
} }
} }
private bool IsNameValid(Client c, string newName) private bool IsNameValid(Client c, string newName)
{ {
newName = Client.SanitizeName(newName); newName = Client.SanitizeName(newName);
@@ -168,6 +168,7 @@ namespace Barotrauma.Networking
if (!bufferedEvent.Character.IsIncapacitated && if (!bufferedEvent.Character.IsIncapacitated &&
NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID)) NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID))
{ {
DebugConsole.Log($"Delaying reading entity event sent by a client until the character state has been processed. Event's character state: {bufferedEvent.CharacterStateID}, last processed character state: {bufferedEvent.Character.LastProcessedID}");
continue; continue;
} }
@@ -250,7 +250,9 @@ namespace Barotrauma.Networking
structToSend = new ServerPeerContentPackageOrderPacket structToSend = new ServerPeerContentPackageOrderPacket
{ {
ServerName = GameMain.Server.ServerName, ServerName = GameMain.Server.ServerName,
ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) ContentPackages = ContentPackageManager.EnabledPackages.All
.Where(cp => cp.Files.Any())
.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile))
.Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
.ToImmutableArray() .ToImmutableArray()
}; };
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.19.11.0</Version> <Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -442,6 +442,7 @@ namespace Barotrauma
base.Update(deltaTime); base.Update(deltaTime);
UpdateTriggers(deltaTime); UpdateTriggers(deltaTime);
Character.ClearInputs(); Character.ClearInputs();
Reverse = false;
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering) if (steeringManager == insideSteering)
@@ -804,10 +805,6 @@ namespace Barotrauma
Reverse = true; Reverse = true;
run = true; run = true;
} }
else
{
Reverse = false;
}
SteeringManager.SteeringManual(deltaTime, dir * 0.2f); SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
} }
else else
@@ -1490,40 +1487,26 @@ namespace Barotrauma
canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle); canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle);
if (canAttack && AttackLimb.attack.AvoidFriendlyFire) if (canAttack && AttackLimb.attack.AvoidFriendlyFire)
{ {
float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); canAttack = !IsBlocked(Character.GetRelativeSimPosition(SelectedAiTarget.Entity));
bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; bool IsBlocked(Vector2 targetPosition)
if (SwarmBehavior != null)
{ {
canAttack = SwarmBehavior.Members.All(c => c == Character || IsFarEnough(c)); foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
}
else
{
canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c));
}
if (canAttack)
{
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range));
bool IsBlocked(Vector2 targetPosition)
{ {
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) Character hitTarget = null;
if (body.UserData is Character c)
{ {
Character hitTarget = null; hitTarget = c;
if (body.UserData is Character c) }
{ else if (body.UserData is Limb limb)
hitTarget = c; {
} hitTarget = limb.character;
else if (body.UserData is Limb limb) }
{ if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
hitTarget = limb.character; {
} return true;
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
{
return true;
}
} }
return false;
} }
return false;
} }
} }
} }
@@ -1854,7 +1837,33 @@ namespace Barotrauma
} }
} }
if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) if (AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
bool advance = !canAttack && Character.InWater || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
{
Reverse = true;
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
else if (advance)
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else if (!Character.InWater)
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
}
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{ {
if (pathSteering != null) if (pathSteering != null)
{ {
@@ -1865,20 +1874,30 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10); SteeringManager.SteeringSeek(steerPos, 10);
} }
} }
else if (AttackLimb.attack.Ranged)
{
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))) if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)))
{ {
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
} }
} }
} }
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack)
{
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
Vector2 toTarget = damageTarget.WorldPosition - limb.WorldPosition;
float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
limb.body.SuppressSmoothRotationCalls = false;
float angle = MathUtils.VectorToAngle(toTarget);
limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque);
limb.body.SuppressSmoothRotationCalls = true;
}
}
if (canAttack) if (canAttack)
{ {
if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb)) if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
{ {
IgnoreTarget(SelectedAiTarget); IgnoreTarget(SelectedAiTarget);
} }
@@ -2114,13 +2133,14 @@ namespace Barotrauma
} }
// 10 dmg, 100 health -> 0.1 // 10 dmg, 100 health -> 0.1
private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); private static float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, float distance = -1, Limb targetLimb = null)
{ {
if (SelectedAiTarget?.Entity == null) { return false; } if (SelectedAiTarget?.Entity == null) { return false; }
if (attackingLimb?.attack == null) { return false; } if (AttackLimb?.attack == null) { return false; }
ActiveAttack = attackingLimb.attack; if (damageTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (wallTarget != null) if (wallTarget != null)
{ {
// If the selected target is not the wall target, make the wall target the selected target. // If the selected target is not the wall target, make the wall target the selected target.
@@ -2131,83 +2151,94 @@ namespace Barotrauma
State = AIState.Attack; State = AIState.Attack;
} }
} }
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; if (damageTarget == null) { return false; }
if (damageTarget != null) if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{ {
if (Character.Params.CanInteract && Character.Inventory != null) Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{ {
// Use equipped items (weapons) Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition;
Item item = GetEquippedItem(attackingLimb); float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
if (item != null) Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
if (angle > ActiveAttack.RequiredAngleToShoot)
{ {
if (item.RequireAimToUse) return true;
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
} }
} }
//simulate attack input to get the character to attack client-side }
Character.SetInput(InputType.Attack, true, true); if (Character.Params.CanInteract && Character.Inventory != null)
if (!ActiveAttack.IsRunning) {
// Use equipped items (weapons)
Item item = GetEquippedItem(AttackLimb);
if (item != null)
{ {
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
}
}
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
{
#if SERVER #if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
attackingLimb, AttackLimb,
damageTarget, damageTarget,
targetLimb, targetLimb,
SimPosition)); SimPosition));
#else #else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif #endif
} }
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (ActiveAttack.CoolDownTimer > 0)
{ {
if (attackingLimb.attack.CoolDownTimer > 0) SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (damageTarget is not Barotrauma.Character)
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
{
LatchOntoAI.SetAttachTarget(targetCharacter);
}
if (!ActiveAttack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{ {
SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed; float greed = AIParams.AggressionGreed;
if (!(damageTarget is Character)) if (damageTarget is not Barotrauma.Character)
{ {
// Halve the greed for attacking non-characters. // Halve the greed for attacking non-characters.
greed /= 2; greed /= 2;
} }
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
} }
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) else
{ {
LatchOntoAI.SetAttachTarget(targetCharacter); selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
} return selectedTargetMemory.Priority > 1;
if (!attackingLimb.attack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (!(damageTarget is Character))
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
else
{
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
} }
} }
return true;
} }
return false; return true;
} }
private float aimTimer; private float aimTimer;
@@ -2299,7 +2330,6 @@ namespace Barotrauma
{ {
if (attackVector == null) if (attackVector == null)
{ {
// TODO: test adding some random variance here?
attackVector = attackWorldPos - WorldPosition; attackVector = attackWorldPos - WorldPosition;
} }
Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
@@ -2319,6 +2349,16 @@ namespace Barotrauma
return true; return true;
} }
private Limb GetLimbToRotate(Attack attack)
{
Limb limb = AttackLimb;
if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex < Character.AnimController.Limbs.Length)
{
limb = Character.AnimController.Limbs[attack.RotationLimbIndex];
}
return limb;
}
#endregion #endregion
#region Eat #region Eat
@@ -3429,7 +3469,7 @@ namespace Barotrauma
private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false)
=> ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting); => ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting);
private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false) private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false)
{ {
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
{ {
@@ -3437,6 +3477,11 @@ namespace Barotrauma
{ {
if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams)) if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams))
{ {
if (state == AIState.Attack)
{
// Only applies to new temp target params. Shouldn't affect any existing definitions (handled below).
targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
}
tempParams.Add(tag, targetParams); tempParams.Add(tag, targetParams);
} }
} }
@@ -3470,7 +3515,7 @@ namespace Barotrauma
{ {
isStateChanged = true; isStateChanged = true;
SetStateResetTimer(); SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority); ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (target.IsHuman) if (target.IsHuman)
{ {
priority = GetTargetParams("human")?.Priority; priority = GetTargetParams("human")?.Priority;
@@ -748,6 +748,9 @@ namespace Barotrauma
} }
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType)) if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
{ {
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
Weapon.TryInteract(character, forceSelectKey: true); Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s)); var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots)) if (character.Inventory.TryPutItem(Weapon, character, slots))
@@ -764,7 +767,7 @@ namespace Barotrauma
} }
return true; return true;
bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
} }
private float findHullTimer; private float findHullTimer;
@@ -186,8 +186,8 @@ namespace Barotrauma
{ {
if (character.SelectedItem != Item) if (character.SelectedItem != Item)
{ {
if (Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) || if (Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true) ||
Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true)) Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true))
{ {
character.SelectedItem = Item; character.SelectedItem = Item;
} }
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using static Barotrauma.CharacterParams;
namespace Barotrauma namespace Barotrauma
{ {
@@ -44,7 +45,7 @@ namespace Barotrauma
public float PlayTimer { get; set; } public float PlayTimer { get; set; }
private float? unstunY { get; set; } private float? unstunY { get; set; }
public EnemyAIController AiController { get; private set; } = null; public EnemyAIController AIController { get; private set; } = null;
public Character Owner { get; set; } public Character Owner { get; set; }
@@ -134,8 +135,8 @@ namespace Barotrauma
aggregate += Items[i].Commonness; aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null) if (aggregate >= r && Items[i].Prefab != null)
{ {
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
break; break;
} }
} }
@@ -160,8 +161,8 @@ namespace Barotrauma
public PetBehavior(XElement element, EnemyAIController aiController) public PetBehavior(XElement element, EnemyAIController aiController)
{ {
AiController = aiController; AIController = aiController;
AiController.Character.CanBeDragged = true; AIController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f); MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f);
MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f); MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f);
@@ -218,7 +219,7 @@ namespace Barotrauma
bool success = OnEat(item.GetTags()); bool success = OnEat(item.GetTags());
if (success) if (success)
{ {
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier); GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier);
} }
return success; return success;
} }
@@ -229,7 +230,7 @@ namespace Barotrauma
bool success = OnEat("dead".ToIdentifier()); bool success = OnEat("dead".ToIdentifier());
if (success) if (success)
{ {
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName);
} }
return success; return success;
} }
@@ -252,7 +253,7 @@ namespace Barotrauma
Hunger += foods[i].Hunger; Hunger += foods[i].Hunger;
Happiness += foods[i].Happiness; Happiness += foods[i].Happiness;
#if CLIENT #if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
#endif #endif
return true; return true;
} }
@@ -265,20 +266,20 @@ namespace Barotrauma
if (PlayTimer > 0.0f) { return; } if (PlayTimer > 0.0f) { return; }
if (Owner == null) { Owner = player; } if (Owner == null) { Owner = player; }
PlayTimer = 5.0f; PlayTimer = 5.0f;
AiController.Character.IsRagdolled = true; AIController.Character.IsRagdolled = true;
Happiness += 10.0f; Happiness += 10.0f;
AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AiController.Character.SimPosition.Y; unstunY = AIController.Character.SimPosition.Y;
#if CLIENT #if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
#endif #endif
} }
public string GetTagName() public string GetTagName()
{ {
if (AiController.Character.Inventory != null) if (AIController.Character.Inventory != null)
{ {
foreach (Item item in AiController.Character.Inventory.AllItems) foreach (Item item in AIController.Character.Inventory.AllItems)
{ {
var tag = item.GetComponent<NameTag>(); var tag = item.GetComponent<NameTag>();
if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName)) if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
@@ -293,7 +294,7 @@ namespace Barotrauma
public void Update(float deltaTime) public void Update(float deltaTime)
{ {
var character = AiController.Character; var character = AIController.Character;
if (character?.Removed ?? true || character.IsDead) { return; } if (character?.Removed ?? true || character.IsDead) { return; }
if (unstunY.HasValue) if (unstunY.HasValue)
@@ -332,16 +333,27 @@ namespace Barotrauma
Food food = foods[i]; Food food = foods[i];
if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y) if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y)
{ {
if (food.TargetParams == null && if (food.TargetParams == null)
AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams))
{ {
targetParams.IgnoreContained = food.IgnoreContained; if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target))
food.TargetParams = targetParams; {
food.TargetParams = target;
}
else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
{
food.TargetParams = targetParams;
}
if (food.TargetParams != null)
{
food.TargetParams.State = AIState.Eat;
food.TargetParams.Priority = food.Priority;
food.TargetParams.IgnoreContained = food.IgnoreContained;
}
} }
} }
else if (food.TargetParams != null) else if (food.TargetParams != null)
{ {
AiController.AIParams.RemoveTarget(food.TargetParams); AIController.AIParams.RemoveTarget(food.TargetParams);
food.TargetParams = null; food.TargetParams = null;
} }
} }
@@ -116,10 +116,10 @@ namespace Barotrauma
} }
// accept only the highest priority order // accept only the highest priority order
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder) if (CurrentOrder == null || OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
{ {
#if DEBUG #if DEBUG
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}"); ShipCommandManager.ShipCommandLog($"{this} is no longer the top priority of {OrderedCharacter}, considering the issue unattended.");
#endif #endif
return false; return false;
} }
@@ -356,7 +356,7 @@ namespace Barotrauma
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order)); ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order));
} }
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret"))) foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint")))
{ {
var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent<Turret>()); var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent<Turret>());
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order));
@@ -87,7 +87,7 @@ namespace Barotrauma
} }
public bool CanWalk => RagdollParams.CanWalk; public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir); public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false };
// TODO: define death anim duration in XML // TODO: define death anim duration in XML
protected float deathAnimTimer, deathAnimDuration = 5.0f; protected float deathAnimTimer, deathAnimDuration = 5.0f;
@@ -610,15 +610,18 @@ namespace Barotrauma
torsoAngle -= herpesStrength / 150.0f; torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
} }
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) if (!head.Disabled)
{ {
float headAngle = HeadAngle.Value; if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } {
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); float headAngle = HeadAngle.Value;
} if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
else head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
{ }
RotateHead(head); else
{
RotateHead(head);
}
} }
if (!onGround) if (!onGround)
@@ -1389,7 +1392,7 @@ namespace Barotrauma
target.Oxygen += deltaTime * 0.5f; //Stabilize them target.Oxygen += deltaTime * 0.5f; //Stabilize them
} }
bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR); float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel("medical"); int skill = (int)character.GetSkillLevel("medical");
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
@@ -1406,7 +1409,7 @@ namespace Barotrauma
{ {
if (target.Oxygen < -10.0f) if (target.Oxygen < -10.0f)
{ {
if (powerfulCPR) if (cprBoost >= 1f)
{ {
//prevent the patient from suffocating no matter how fast their oxygen level is dropping //prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f); target.Oxygen = Math.Max(target.Oxygen, -10.0f);
@@ -1453,7 +1456,7 @@ namespace Barotrauma
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
if (powerfulCPR) { reviveChance *= 2.0f; } reviveChance *= 1f + cprBoost;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{ {
@@ -873,7 +873,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs) foreach (Limb limb in Limbs)
{ {
if (limb == null || limb.IsSevered) { continue; } if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; }
limb.Dir = Dir; limb.Dir = Dir;
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
limb.MirrorPullJoint(); limb.MirrorPullJoint();
@@ -1337,7 +1337,7 @@ namespace Barotrauma
bool limbsValid = true; bool limbsValid = true;
foreach (Limb limb in limbs) foreach (Limb limb in limbs)
{ {
if (limb.body == null || !limb.body.Enabled) { continue; } if (limb?.body == null || !limb.body.Enabled) { continue; }
if (!CheckValidity(limb.body)) if (!CheckValidity(limb.body))
{ {
limbsValid = false; limbsValid = false;
@@ -1959,7 +1959,7 @@ namespace Barotrauma
{ {
foreach (Limb l in Limbs) foreach (Limb l in Limbs)
{ {
l.Remove(); l?.Remove();
} }
limbs = null; limbs = null;
} }
@@ -1968,7 +1968,7 @@ namespace Barotrauma
{ {
foreach (PhysicsBody b in collider) foreach (PhysicsBody b in collider)
{ {
b.Remove(); b?.Remove();
} }
collider = null; collider = null;
} }
@@ -1977,7 +1977,7 @@ namespace Barotrauma
{ {
foreach (var joint in LimbJoints) foreach (var joint in LimbJoints)
{ {
var j = joint.Joint; var j = joint?.Joint;
if (GameMain.World.JointList.Contains(j)) if (GameMain.World.JointList.Contains(j))
{ {
GameMain.World.Remove(j); GameMain.World.Remove(j);
@@ -189,6 +189,15 @@ namespace Barotrauma
[Serialize(20f, IsPropertySaveable.Yes)] [Serialize(20f, IsPropertySaveable.Yes)]
public float RequiredAngle { get; set; } public float RequiredAngle { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
public float RequiredAngleToShoot { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the attack limb is rotated towards the target. Default 0 = no rotation. Only affects ranged attacks."), Editable]
public float AimRotationTorque { get; set; }
[Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable]
public int RotationLimbIndex { get; set; }
/// <summary> /// <summary>
/// Legacy support. Use Afflictions. /// Legacy support. Use Afflictions.
/// </summary> /// </summary>
@@ -529,6 +538,12 @@ namespace Barotrauma
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition); effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition);
} }
} }
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
} }
return attackResult; return attackResult;
@@ -591,6 +606,12 @@ namespace Barotrauma
{ {
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition); effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
} }
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
} }
return attackResult; return attackResult;
@@ -129,6 +129,7 @@ namespace Barotrauma
public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active); public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active);
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
public bool IsEscorted { get; set; } public bool IsEscorted { get; set; }
public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
public readonly Dictionary<Identifier, SerializableProperty> Properties; public readonly Dictionary<Identifier, SerializableProperty> Properties;
public Dictionary<Identifier, SerializableProperty> SerializableProperties public Dictionary<Identifier, SerializableProperty> SerializableProperties
@@ -611,7 +612,9 @@ namespace Barotrauma
CharacterHealth.SetHealthBarVisibility(value == null); CharacterHealth.SetHealthBarVisibility(value == null);
#endif #endif
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value));
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet })
{ {
#if SERVER #if SERVER
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings }) if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings })
@@ -999,7 +1002,7 @@ namespace Barotrauma
} }
} }
public bool InWater => AnimController?.InWater ?? false; public bool InWater => AnimController is AnimController { InWater: true };
public bool GodMode = false; public bool GodMode = false;
@@ -1053,6 +1056,8 @@ namespace Barotrauma
} }
} }
public HashSet<Identifier> MarkedAsLooted = new();
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath); public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
@@ -1574,7 +1579,7 @@ namespace Barotrauma
} }
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{ {
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()])); GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
} }
} }
} }
@@ -1582,6 +1587,15 @@ namespace Barotrauma
public float GetSkillLevel(string skillIdentifier) => public float GetSkillLevel(string skillIdentifier) =>
GetSkillLevel(skillIdentifier.ToIdentifier()); GetSkillLevel(skillIdentifier.ToIdentifier());
private static readonly ImmutableDictionary<Identifier, StatTypes> overrideStatTypes = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillOverride },
{ new("medical"), StatTypes.MedicalSkillOverride },
{ new("weapons"), StatTypes.WeaponsSkillOverride },
{ new("electrical"), StatTypes.ElectricalSkillOverride },
{ new("mechanical"), StatTypes.MechanicalSkillOverride }
}.ToImmutableDictionary();
public float GetSkillLevel(Identifier skillIdentifier) public float GetSkillLevel(Identifier skillIdentifier)
{ {
if (Info?.Job == null) { return 0.0f; } if (Info?.Job == null) { return 0.0f; }
@@ -1617,6 +1631,16 @@ namespace Barotrauma
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
{
float skillOverride = GetStatValue(statType);
if (skillOverride > skillLevel)
{
skillLevel = skillOverride;
}
}
return skillLevel; return skillLevel;
} }
@@ -2058,30 +2082,42 @@ namespace Barotrauma
{ {
foreach (Item item in HeldItems) foreach (Item item in HeldItems)
{ {
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse) tryUseItem(item, deltaTime);
}
foreach (Item item in Inventory.AllItems)
{
if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
{ {
item.SecondaryUse(deltaTime, this); tryUseItem(item, deltaTime);
} }
if (IsKeyDown(InputType.Use) && !item.IsShootable) }
}
void tryUseItem(Item item, float deltaTime)
{
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
{
item.SecondaryUse(deltaTime, this);
}
if (IsKeyDown(InputType.Use) && !item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{ {
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) item.Use(deltaTime, this);
{
item.Use(deltaTime, this);
}
} }
if (IsKeyDown(InputType.Shoot) && item.IsShootable) }
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{ {
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) item.Use(deltaTime, this);
{ }
item.Use(deltaTime, this);
}
#if CLIENT #if CLIENT
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{ {
HintManager.OnShootWithoutAiming(this, item); HintManager.OnShootWithoutAiming(this, item);
}
#endif
} }
#endif
} }
} }
@@ -2721,6 +2757,11 @@ namespace Barotrauma
} }
} }
bool selectInputSameAsDeselect = false;
#if CLIENT
selectInputSameAsDeselect = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Deselect];
#endif
if (SelectedCharacter != null && (IsKeyHit(InputType.Grab) || IsKeyHit(InputType.Health))) //Let people use ladders and buttons and stuff when dragging chars if (SelectedCharacter != null && (IsKeyHit(InputType.Grab) || IsKeyHit(InputType.Health))) //Let people use ladders and buttons and stuff when dragging chars
{ {
DeselectCharacter(); DeselectCharacter();
@@ -2760,14 +2801,16 @@ namespace Barotrauma
{ {
FocusedCharacter.onCustomInteract(FocusedCharacter, this); FocusedCharacter.onCustomInteract(FocusedCharacter, this);
} }
else if (IsKeyHit(InputType.Deselect) && SelectedItem != null) else if (IsKeyHit(InputType.Deselect) && SelectedItem != null &&
(focusedItem == null || focusedItem == SelectedItem || !selectInputSameAsDeselect))
{ {
SelectedItem = null; SelectedItem = null;
#if CLIENT #if CLIENT
CharacterHealth.OpenHealthWindow = null; CharacterHealth.OpenHealthWindow = null;
#endif #endif
} }
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null) else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent<Ladder>() == null &&
(focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect))
{ {
SelectedSecondaryItem = null; SelectedSecondaryItem = null;
#if CLIENT #if CLIENT
@@ -2782,6 +2825,10 @@ namespace Barotrauma
{ {
#if CLIENT #if CLIENT
if (CharacterInventory.DraggingItemToWorld) { return; } if (CharacterInventory.DraggingItemToWorld) { return; }
if (selectInputSameAsDeselect)
{
keys[(int)InputType.Deselect].Reset();
}
#endif #endif
bool canInteract = focusedItem.TryInteract(this); bool canInteract = focusedItem.TryInteract(this);
#if CLIENT #if CLIENT
@@ -3787,7 +3834,7 @@ namespace Barotrauma
return; return;
} }
#endif #endif
if (damage < targetLimb.Params.MinSeveranceDamage) { return; } if (damage > 0 && damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead) if (!IsDead)
{ {
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; } if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
@@ -3805,7 +3852,7 @@ namespace Barotrauma
var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB; var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB;
if (referenceLimb != targetLimb) { continue; } if (referenceLimb != targetLimb) { continue; }
float probability = severLimbsProbability; float probability = severLimbsProbability;
if (!IsDead) if (!IsDead && probability < 1)
{ {
probability *= joint.Params.SeveranceProbabilityModifier; probability *= joint.Params.SeveranceProbabilityModifier;
} }
@@ -4778,6 +4825,32 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier); return info.UnlockedTalents.Contains(identifier);
} }
private readonly HashSet<Hull> sameRoomHulls = new();
/// <summary>
/// Check if the character is in the same room
/// Room and hull differ in that a room can consist of multiple linked hulls
/// </summary>
public bool IsInSameRoomAs(Character character)
{
if (character == this) { return true; }
if (character.CurrentHull is null || CurrentHull is null)
{
// Outside doesn't count as a room
return false;
}
if (character.Submarine != Submarine) { return false; }
if (character.CurrentHull == CurrentHull) { return true; }
sameRoomHulls.Clear();
CurrentHull.GetLinkedEntities(sameRoomHulls);
sameRoomHulls.Add(CurrentHull);
return sameRoomHulls.Contains(character.CurrentHull);
}
public bool HasUnlockedAllTalents() public bool HasUnlockedAllTalents()
{ {
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
@@ -4786,7 +4859,7 @@ namespace Barotrauma
{ {
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{ {
if (talentOption.TalentIdentifiers.None(t => HasTalent(t))) if (talentOption.TalentIdentifiers.None(HasTalent))
{ {
return false; return false;
} }
@@ -4831,6 +4904,19 @@ namespace Barotrauma
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
} }
public bool HasStoreAccessForItem(ItemPrefab prefab)
{
foreach (CharacterTalent talent in characterTalents)
{
foreach (Identifier unlockedItem in talent.UnlockedStoreItems)
{
if (prefab.Tags.Contains(unlockedItem)) { return true; }
}
}
return false;
}
/// <summary> /// <summary>
/// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains. /// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains.
/// </summary> /// </summary>
@@ -5043,6 +5129,16 @@ namespace Barotrauma
} }
} }
internal sealed class AbilityCharacterLoot : AbilityObject, IAbilityCharacter
{
public Character Character { get; set; }
public AbilityCharacterLoot(Character character)
{
Character = character;
}
}
class AbilityCharacterKill : AbilityObject, IAbilityCharacter class AbilityCharacterKill : AbilityObject, IAbilityCharacter
{ {
public AbilityCharacterKill(Character character, Character killer) public AbilityCharacterKill(Character character, Character killer)
@@ -543,7 +543,7 @@ namespace Barotrauma
private void GetName(Rand.RandSync randSync, out string name) private void GetName(Rand.RandSync randSync, out string name)
{ {
var nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name"); ContentXElement nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name");
ContentPath namesXmlFile = nameElement?.GetAttributeContentPath("path") ?? ContentPath.Empty; ContentPath namesXmlFile = nameElement?.GetAttributeContentPath("path") ?? ContentPath.Empty;
XElement namesXml = null; XElement namesXml = null;
if (!namesXmlFile.IsNullOrEmpty()) //names.xml is defined if (!namesXmlFile.IsNullOrEmpty()) //names.xml is defined
@@ -554,8 +554,8 @@ namespace Barotrauma
else //the legacy firstnames.txt/lastnames.txt shit is defined else //the legacy firstnames.txt/lastnames.txt shit is defined
{ {
namesXml = new XElement("names", new XAttribute("format", "[firstname] [lastname]")); namesXml = new XElement("names", new XAttribute("format", "[firstname] [lastname]"));
var firstNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? ""); string firstNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? "");
var lastNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? ""); string lastNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? "");
if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath)) if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath))
{ {
var firstNames = File.ReadAllLines(firstNamesPath); var firstNames = File.ReadAllLines(firstNamesPath);
@@ -735,9 +735,7 @@ namespace Barotrauma
Name = infoElement.GetAttributeString("name", ""); Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null); OriginalName = infoElement.GetAttributeString("originalname", null);
Salary = infoElement.GetAttributeInt("salary", 1000); Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
UnlockedTalents = new HashSet<Identifier>(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty<Identifier>()));
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet(); HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags); LoadTagsBackwardsCompatibility(infoElement, tags);
@@ -813,18 +811,22 @@ namespace Barotrauma
infoElement.GetAttributeIdentifier("npcid", Identifier.Empty)); infoElement.GetAttributeIdentifier("npcid", Identifier.Empty));
MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0);
UnlockedTalents = new HashSet<Identifier>();
foreach (var subElement in infoElement.Elements()) foreach (var subElement in infoElement.Elements())
{ {
bool jobCreated = false; bool jobCreated = false;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated)
Identifier elementName = subElement.Name.ToIdentifier();
if (elementName == "job" && !jobCreated)
{ {
Job = new Job(subElement); Job = new Job(subElement);
jobCreated = true; jobCreated = true;
// there used to be a break here, but it had to be removed to make room for statvalues // there used to be a break here, but it had to be removed to make room for statvalues
// using the jobCreated boolean to make sure that only the first job found is created // using the jobCreated boolean to make sure that only the first job found is created
} }
else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase)) else if (elementName == "savedstatvalues")
{ {
foreach (XElement savedStat in subElement.Elements()) foreach (XElement savedStat in subElement.Elements())
{ {
@@ -838,8 +840,8 @@ namespace Barotrauma
float value = savedStat.GetAttributeFloat("statvalue", 0f); float value = savedStat.GetAttributeFloat("statvalue", 0f);
if (value == 0f) { continue; } if (value == 0f) { continue; }
string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant(); Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (string.IsNullOrEmpty(statIdentifier)) if (statIdentifier.IsEmpty)
{ {
DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!"); DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!");
return; return;
@@ -849,6 +851,20 @@ namespace Barotrauma
ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath); ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath);
} }
} }
else if (elementName == "talents")
{
Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe
foreach (XElement talentElement in subElement.Elements())
{
if (talentElement.Name.ToIdentifier() != "talent") { continue; }
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (talentIdentifier == Identifier.Empty) { continue; }
UnlockedTalents.Add(talentIdentifier);
}
}
} }
LoadHeadAttachments(); LoadHeadAttachments();
} }
@@ -1149,6 +1165,8 @@ namespace Barotrauma
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed); increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier); float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum)); Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
@@ -1156,6 +1174,8 @@ namespace Barotrauma
if ((int)newLevel > (int)prevLevel) if ((int)newLevel > (int)prevLevel)
{ {
float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain);
Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
// assume we are getting at least 1 point in skill, since this logic only runs in such cases // assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f); float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility); var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility);
@@ -1169,6 +1189,25 @@ namespace Barotrauma
OnSkillChanged(skillIdentifier, prevLevel, newLevel); OnSkillChanged(skillIdentifier, prevLevel, newLevel);
} }
private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillGainSpeed },
{ new("medical"), StatTypes.WeaponsSkillGainSpeed },
{ new("weapons"), StatTypes.MedicalSkillGainSpeed },
{ new("electrical"), StatTypes.ElectricalSkillGainSpeed },
{ new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
}.ToImmutableDictionary();
private float GetSkillSpecificGain(float increase, Identifier skillIdentifier)
{
if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType))
{
increase *= 1f + Character.GetStatValue(statType);
}
return increase;
}
public void SetSkillLevel(Identifier skillIdentifier, float level) public void SetSkillLevel(Identifier skillIdentifier, float level)
{ {
if (Job == null) { return; } if (Job == null) { return; }
@@ -1314,7 +1353,6 @@ namespace Barotrauma
new XAttribute("tags", string.Join(",", Head.Preset.TagSet)), new XAttribute("tags", string.Join(",", Head.Preset.TagSet)),
new XAttribute("salary", Salary), new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints), new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints), new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute("hairindex", Head.HairIndex), new XAttribute("hairindex", Head.HairIndex),
new XAttribute("beardindex", Head.BeardIndex), new XAttribute("beardindex", Head.BeardIndex),
@@ -1363,7 +1401,16 @@ namespace Barotrauma
} }
} }
XElement talentElement = new XElement("Talents");
talentElement.Add(new XAttribute("version", GameMain.Version.ToString()));
foreach (Identifier talentIdentifier in UnlockedTalents)
{
talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
}
charElement.Add(savedStatElement); charElement.Add(savedStatElement);
charElement.Add(talentElement);
parentElement?.Add(charElement); parentElement?.Add(charElement);
return charElement; return charElement;
} }
@@ -1717,20 +1764,33 @@ namespace Barotrauma
} }
} }
public void ResetSavedStatValue(string statIdentifier) public void ResetSavedStatValue(Identifier statIdentifier)
{ {
foreach (StatTypes statType in SavedStatValues.Keys) foreach (StatTypes statType in SavedStatValues.Keys)
{ {
bool changed = false; bool changed = false;
foreach (SavedStatValue savedStatValue in SavedStatValues[statType]) foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{ {
if (savedStatValue.StatIdentifier != statIdentifier) { continue; } if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; } if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f; savedStatValue.StatValue = 0.0f;
changed = true; changed = true;
} }
if (changed) { OnPermanentStatChanged(statType); } if (changed) { OnPermanentStatChanged(statType); }
} }
static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier)
{
if (statIdentifier == identifier) { return true; }
if (identifier.IndexOf('*') is var index and > -1)
{
return statIdentifier.StartsWith(identifier[0..index]);
}
return false;
}
} }
public float GetSavedStatValue(StatTypes statType) public float GetSavedStatValue(StatTypes statType)
@@ -1756,7 +1816,7 @@ namespace Barotrauma
} }
} }
public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{ {
if (!SavedStatValues.ContainsKey(statType)) if (!SavedStatValues.ContainsKey(statType))
{ {
@@ -1779,13 +1839,13 @@ namespace Barotrauma
} }
} }
public class SavedStatValue internal sealed class SavedStatValue
{ {
public string StatIdentifier { get; set; } public Identifier StatIdentifier { get; set; }
public float StatValue { get; set; } public float StatValue { get; set; }
public bool RemoveOnDeath { get; set; } public bool RemoveOnDeath { get; set; }
public SavedStatValue(string statIdentifier, float value, bool removeOnDeath) public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath)
{ {
StatValue = value; StatValue = value;
RemoveOnDeath = removeOnDeath; RemoveOnDeath = removeOnDeath;
@@ -1793,7 +1853,7 @@ namespace Barotrauma
} }
} }
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{ {
public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility) public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility)
{ {
@@ -384,6 +384,8 @@ namespace Barotrauma
private readonly ConstructorInfo constructor; private readonly ConstructorInfo constructor;
public readonly bool ResetBetweenRounds;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
{ {
get get
@@ -465,6 +467,8 @@ namespace Barotrauma
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false); AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", ""); AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", "");
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
switch (subElement.Name.ToString().ToLowerInvariant()) switch (subElement.Name.ToString().ToLowerInvariant())
@@ -142,7 +142,18 @@ namespace Barotrauma
{ {
get get
{ {
return Character.IsDead ? minVitality : vitality; if (Character.IsDead)
{
return minVitality;
}
if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
return Math.Max(vitality, MinVitality + 1);
}
return vitality;
} }
private set private set
{ {
@@ -881,6 +892,9 @@ namespace Barotrauma
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
decreaseSpeed *= (1f - oxygenlowResistance); decreaseSpeed *= (1f - oxygenlowResistance);
increaseSpeed *= (1f + oxygenlowResistance); increaseSpeed *= (1f + oxygenlowResistance);
float holdBreathMultiplier = 1f + GetStatValue(StatTypes.HoldBreathMultiplier);
decreaseSpeed *= holdBreathMultiplier;
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f); OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
} }
@@ -1217,6 +1231,7 @@ namespace Barotrauma
var affliction = kvp.Key; var affliction = kvp.Key;
var limbHealth = kvp.Value; var limbHealth = kvp.Value;
if (affliction.Strength <= 0.0f || limbHealth != null) { continue; } if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
healthElement.Add(new XElement("Affliction", healthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier), new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture)))); new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
@@ -778,6 +778,7 @@ namespace Barotrauma
{ {
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character); var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter); attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
newAffliction = abilityAfflictionCharacter.Affliction;
} }
if (applyAffliction) if (applyAffliction)
{ {
@@ -896,6 +897,12 @@ namespace Barotrauma
{ {
reEnableTimer = duration; reEnableTimer = duration;
} }
#if CLIENT
if (Hidden && LightSource != null)
{
LightSource.Enabled = false;
}
#endif
} }
public void ReEnable() public void ReEnable()
@@ -1194,7 +1201,25 @@ namespace Barotrauma
} }
else else
{ {
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory)
{
foreach (Item item in inventory.AllItems)
{
if (statusEffect.TargetIdentifiers != null &&
!statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) &&
statusEffect.TargetIdentifiers.None(id => item.HasTag(id)))
{
continue;
}
if (statusEffect.TargetSlot > -1)
{
if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; }
}
targets.Add(item);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{ {
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
} }
@@ -649,7 +649,7 @@ namespace Barotrauma
if (HasTag(tag)) if (HasTag(tag))
{ {
target = null; target = null;
DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
return false; return false;
} }
else else
@@ -1,8 +1,5 @@
using Microsoft.Xna.Framework; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -34,6 +31,7 @@ namespace Barotrauma.Abilities
Alive = 4, Alive = 4,
Monster = 5, Monster = 5,
InFriendlySubmarine = 6, InFriendlySubmarine = 6,
Large = 7,
}; };
protected List<TargetType> ParseTargetTypes(string[] targetTypeStrings) protected List<TargetType> ParseTargetTypes(string[] targetTypeStrings)
@@ -41,8 +39,7 @@ namespace Barotrauma.Abilities
List<TargetType> targetTypes = new List<TargetType>(); List<TargetType> targetTypes = new List<TargetType>();
foreach (string targetTypeString in targetTypeStrings) foreach (string targetTypeString in targetTypeStrings)
{ {
TargetType targetType = TargetType.Any; if (!Enum.TryParse(targetTypeString, true, out TargetType targetType))
if (!Enum.TryParse(targetTypeString, true, out targetType))
{ {
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
} }
@@ -83,6 +80,9 @@ namespace Barotrauma.Abilities
return !targetCharacter.IsHuman; return !targetCharacter.IsHuman;
case TargetType.InFriendlySubmarine: case TargetType.InFriendlySubmarine:
return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID; return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID;
case TargetType.Large:
// mass of mudraptor is ~48
return targetCharacter.AnimController is { Mass: > 50.0f };
default: default:
return true; return true;
} }
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
@@ -8,11 +9,13 @@ namespace Barotrauma.Abilities
{ {
private readonly List<TargetType> targetTypes; private readonly List<TargetType> targetTypes;
private List<PropertyConditional> conditionals = new List<PropertyConditional>(); private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{ {
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>(), convertToLowerInvariant: true)); targetTypes = ParseTargetTypes(
conditionElement.GetAttributeStringArray("targettypes",
conditionElement.GetAttributeStringArray("targettype", Array.Empty<string>())));
foreach (XElement subElement in conditionElement.Elements()) foreach (XElement subElement in conditionElement.Elements())
{ {
@@ -28,13 +31,18 @@ namespace Barotrauma.Abilities
break; break;
} }
} }
if (!targetTypes.Any() && !conditionals.Any())
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.");
}
} }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityObject is IAbilityCharacter abilityCharacter) if (abilityObject is IAbilityCharacter abilityCharacter)
{ {
if (!(abilityCharacter.Character is Character character)) { return false; } if (abilityCharacter.Character is not Character character) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; } if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals) foreach (var conditional in conditionals)
{ {
@@ -0,0 +1,19 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
{
private readonly Identifier identifier;
public AbilityConditionCharacterNotLooted(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter ability) { return false; }
return !ability.Character.MarkedAsLooted.Contains(identifier);
}
}
}
@@ -0,0 +1,16 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
{
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
return targetCharacter.Character.IsUnconscious;
}
}
}
@@ -1,6 +1,5 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -13,6 +12,11 @@ namespace Barotrauma.Abilities
{ {
identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true); identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true); tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
if (!identifiers.Any() && !tags.Any())
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers or tags defined.");
}
} }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
@@ -8,6 +8,7 @@ namespace Barotrauma.Abilities
{ {
private readonly bool? hasOutpost; private readonly bool? hasOutpost;
private readonly Identifier[] locationIdentifiers; private readonly Identifier[] locationIdentifiers;
private readonly bool isPositiveReputation;
public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{ {
@@ -16,12 +17,19 @@ namespace Barotrauma.Abilities
hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false);
} }
locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>()); locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>());
isPositiveReputation = conditionElement.GetAttributeBool("ispositivereputation", false);
} }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if (abilityObject is IAbilityLocation abilityLocation) if (abilityObject is IAbilityLocation abilityLocation)
{ {
if (isPositiveReputation)
{
if (abilityLocation.Location.Reputation.Faction.Reputation.Value <= 0) { return false; }
}
if (locationIdentifiers.Any()) if (locationIdentifiers.Any())
{ {
if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; } if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; }
@@ -1,38 +1,50 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
class AbilityConditionMission : AbilityConditionData class AbilityConditionMission : AbilityConditionData
{ {
private readonly MissionType missionType; private readonly ImmutableHashSet<MissionType> missionType;
private readonly bool isAffiliated;
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{ {
string missionTypeString = conditionElement.GetAttributeString("missiontype", "None"); string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
if (!Enum.TryParse(missionTypeString, out missionType)) HashSet<MissionType> missionTypes = new HashSet<MissionType>();
foreach (string missionTypeString in missionTypeStrings)
{ {
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type."); if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
return; {
} DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
if (missionType == MissionType.None) return;
{ }
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none.");
return; missionTypes.Add(parsedMission);
} }
missionType = missionTypes.ToImmutableHashSet();
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
} }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject) protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{ {
if ((abilityObject as IAbilityMission)?.Mission is Mission mission) if (abilityObject is IAbilityMission { Mission: { } mission })
{ {
return mission.Prefab.Type == missionType; if (isAffiliated && GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is { } highestFaction)
} {
else if (highestFaction.Reputation.Value < 0 || !mission.ReputationRewards.ContainsKey(highestFaction.Reputation.Identifier))
{ {
LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); return false;
return false; }
}
return missionType.Contains(mission.Prefab.Type);
} }
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
} }
} }
} }
@@ -1,5 +1,4 @@
using System; using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -0,0 +1,47 @@
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionAllyNearby : AbilityConditionDataless
{
private enum NearbyCharacterTruthy
{
OneCharacterMatches,
NoCharacterMatches
}
private readonly NearbyCharacterTruthy truthyWhen;
private readonly float distance;
public AbilityConditionAllyNearby(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
truthyWhen = conditionElement.GetAttributeEnum("truthywhen", NearbyCharacterTruthy.OneCharacterMatches);
distance = conditionElement.GetAttributeFloat("distance", 10f);
}
protected override bool MatchesConditionSpecific()
{
bool trueCondition = truthyWhen switch
{
NearbyCharacterTruthy.OneCharacterMatches => true,
NearbyCharacterTruthy.NoCharacterMatches => false,
_ => throw new ArgumentOutOfRangeException(nameof(truthyWhen))
};
foreach (Character ally in Character.GetFriendlyCrew(character))
{
if (ally == character) { continue; }
float distanceToCharacter = Vector2.DistanceSquared(ally.WorldPosition, character.WorldPosition);
if (distanceToCharacter < distance * distance)
{
return trueCondition;
}
}
return !trueCondition;
}
}
}
@@ -0,0 +1,22 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCrewMemberUnconscious : AbilityConditionDataless
{
public AbilityConditionCrewMemberUnconscious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (c.IsUnconscious)
{
return true;
}
}
return false;
}
}
}
@@ -17,7 +17,7 @@
{ {
var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier); var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier);
if (affliction == null) { return false; } if (affliction == null) { return false; }
return minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength; return affliction.Strength >= affliction.Prefab.ActivationThreshold && minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength;
} }
return false; return false;
} }
@@ -22,7 +22,7 @@ namespace Barotrauma.Abilities
{ {
if (tags.None()) if (tags.None())
{ {
return character.GetEquippedItem(null) is Item; return character.GetEquippedItem(null) != null;
} }
if (requireAll) if (requireAll)
@@ -0,0 +1,43 @@
#nullable enable
using System;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionHasLevel : AbilityConditionDataless
{
private readonly Option<int> matchedLevel;
private readonly Option<int> minLevel;
public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
matchedLevel = conditionElement.GetAttributeInt("levelequals", 0) is var match and not 0
? Option<int>.Some(match)
: Option<int>.None();
minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0
? Option<int>.Some(min)
: Option<int>.None();
if (matchedLevel.IsNone() && minLevel.IsNone())
{
throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute.");
}
}
protected override bool MatchesConditionSpecific()
{
if (matchedLevel.TryUnwrap(out int match))
{
return character.Info.GetCurrentLevel() == match;
}
if (minLevel.TryUnwrap(out int min))
{
return character.Info.GetCurrentLevel() >= min;
}
return false;
}
}
}
@@ -1,13 +1,11 @@
using System.Linq; namespace Barotrauma.Abilities
using System.Xml.Linq;
namespace Barotrauma.Abilities
{ {
class AbilityConditionHasPermanentStat : AbilityConditionDataless class AbilityConditionHasPermanentStat : AbilityConditionDataless
{ {
private readonly Identifier statIdentifier; private readonly Identifier statIdentifier;
private readonly StatTypes statType; private readonly StatTypes statType;
private readonly float min; private readonly float min;
private readonly PermanentStatPlaceholder placeholder;
public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{ {
@@ -19,11 +17,14 @@ namespace Barotrauma.Abilities
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty); string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
min = conditionElement.GetAttributeFloat("min", 0f); min = conditionElement.GetAttributeFloat("min", 0f);
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
} }
protected override bool MatchesConditionSpecific() protected override bool MatchesConditionSpecific()
{ {
return character.Info.GetSavedStatValue(statType, statIdentifier) >= min; Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More