diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 9ec4022c3..1858f969c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -277,7 +277,7 @@ namespace Barotrauma velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); moveCam = velocity * moveSpeed * deltaTime * FreeCamMoveSpeed * 60.0f; - if (Screen.Selected == GameMain.GameScreen && (followSub ?? FollowSub)) + if (Screen.Selected == GameMain.GameScreen && (followSub ?? FollowSub) && GameMain.Instance is not { Paused: true }) { var closestSub = Submarine.FindClosest(WorldViewCenter); if (closestSub != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index c09d92817..e6b91667c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -109,7 +109,7 @@ namespace Barotrauma } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); - float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; + float errorTolerance = character.CanMove && (!character.IsRagdolled || character.AnimController.IsHangingWithRope) ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { if (distSqrd > 10.0f || !character.CanMove) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 8df8deea0..3b5ff2c41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.Particles; @@ -9,7 +10,6 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -42,6 +42,8 @@ namespace Barotrauma private set; } = true; + public bool ShowInteractionLabels { get; private set; } + //the Character that the player is currently controlling private static Character controlled; @@ -230,14 +232,51 @@ namespace Barotrauma } } - private float pressureEffectTimer; - private readonly List activeObjectiveEntities = new List(); public IEnumerable ActiveObjectiveEntities { get { return activeObjectiveEntities; } } + private static readonly List speechBubbles = new List(); + + private SpeechBubble textlessSpeechBubble; + + sealed class SpeechBubble + { + public float LifeTime; + public Vector2 PrevPosition; + public Vector2 Position; + public Vector2 DrawPosition; + public float MoveUpAmount; + public readonly string Text; + public readonly Character Character; + public readonly Submarine Submarine; + public readonly Vector2 TextSize; + + public Color Color; + public bool Moving; + + public SpeechBubble(Character character, float lifeTime, Color color, string text = "") + { + Text = ToolBox.WrapText(text, GUI.IntScale(300), GUIStyle.SmallFont.GetFontForStr(text)); + TextSize = GUIStyle.SmallFont.MeasureString(Text); + + Character = character; + Position = GetDesiredPosition(); + Submarine = character.Submarine; + LifeTime = lifeTime; + Color = color; + } + + public Vector2 GetDesiredPosition() + { + return Character.Position + Vector2.UnitY * 100; + } + } + + private float pressureEffectTimer; + partial void InitProjSpecific(ContentXElement mainElement) { soundTimer = Rand.Range(0.0f, Params.SoundInterval); @@ -272,6 +311,9 @@ namespace Barotrauma } } + private readonly List previousInteractablesInRange = new(); + private readonly List interactablesInRange = new(); + private bool wasFiring; /// @@ -279,6 +321,7 @@ namespace Barotrauma /// public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true) { + if (DisableControls || GUI.InputBlockingMenuOpen) { foreach (Key key in keys) @@ -316,6 +359,13 @@ namespace Barotrauma } } + ShowInteractionLabels = keys[(int)InputType.ShowInteractionLabels].Held; + + if (ShowInteractionLabels) + { + focusedItem = InteractionLabelManager.HoveredItem; + } + //if we were firing (= pressing the aim and shoot keys at the same time) //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) @@ -549,19 +599,61 @@ namespace Barotrauma if (Lights.LightManager.ViewTarget == this) { Lights.LightManager.ViewTarget = null; } } + private void UpdateInteractablesInRange() + { + // keep two lists to detect changes to the current state of interactables in range + previousInteractablesInRange.Clear(); + previousInteractablesInRange.AddRange(interactablesInRange); + + interactablesInRange.Clear(); + + //use the list of visible entities if it exists + var entityList = Submarine.VisibleEntities ?? Item.ItemList; + + foreach (MapEntity entity in entityList) + { + if (entity is not Item item) { continue; } + + if (item.body != null && !item.body.Enabled) { continue; } + + if (item.ParentInventory != null) { continue; } + + if (item.Prefab.RequireCampaignInteract && + item.CampaignInteractionType == CampaignMode.InteractionType.None) + { + continue; + } + + if (Screen.Selected is SubEditorScreen { WiringMode: true } && + item.GetComponent() == null) + { + continue; + } + + if (CanInteractWith(item)) + { + interactablesInRange.Add(item); + } + } + + if (!interactablesInRange.SequenceEqual(previousInteractablesInRange)) + { + InteractionLabelManager.RefreshInteractablesInRange(interactablesInRange); + } + } private readonly List debugInteractablesInRange = new List(); private readonly List debugInteractablesAtCursor = new List(); private readonly List<(Item item, float dist)> debugInteractablesNearCursor = new List<(Item item, float dist)>(); /// - /// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item. + /// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item. /// - /// The Character who is looking for the interactable item, only items that are close enough to this character are returned - /// The item at the simPosition, with the lowest depth, is returned - /// If this is true and an item cannot be found at simPosition then a nearest item will be returned if possible - /// If a hull is specified, only items within that hull are returned - public Item FindItemAtPosition(Vector2 simPosition, float aimAssistModifier = 0.0f, Item[] ignoredItems = null) + /// Item collection to look in + /// sim position for distance comparison (such as mouse position) + /// aim assist modifier + /// + public Item FindClosestItem(List itemCollection, Vector2 simPosition, float aimAssistModifier = 0.0f) { if (Submarine != null) { @@ -580,24 +672,11 @@ namespace Barotrauma float aimAssistAmount = SelectedItem == null ? 100.0f * aimAssistModifier : 1.0f; Vector2 displayPosition = ConvertUnits.ToDisplayUnits(simPosition); - - //use the list of visible entities if it exists - var entityList = Submarine.VisibleEntities ?? Item.ItemList; - + Item closestItem = null; float closestItemDistance = Math.Max(aimAssistAmount, 2.0f); - foreach (MapEntity entity in entityList) + foreach (var item in itemCollection) { - if (entity is not Item item) - { - continue; - } - if (item.body != null && !item.body.Enabled) { continue; } - if (item.ParentInventory != null) { continue; } - if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } - if (item.Prefab.RequireCampaignInteract && item.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; } - if (Screen.Selected is SubEditorScreen editor && editor.WiringMode && item.GetComponent() == null) { continue; } - if (draggingItemToWorld) { if (item.OwnInventory == null || @@ -644,7 +723,7 @@ namespace Barotrauma distanceToItem = 2.0f + Vector2.Distance(rectIntersectionPoint, displayPosition); } } - + if (distanceToItem > closestItemDistance) { continue; } if (!CanInteractWith(item)) { continue; } @@ -661,7 +740,7 @@ namespace Barotrauma Character closestCharacter = null; maxDist = ConvertUnits.ToSimUnits(maxDist); - float closestDist = maxDist * maxDist; + float closestDist = maxDist; foreach (Character c in CharacterList) { if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } @@ -704,6 +783,12 @@ namespace Barotrauma } guiMessages.RemoveAll(m => m.Timer >= m.Lifetime); + if (textlessSpeechBubble != null) + { + textlessSpeechBubble.LifeTime -= deltaTime; + if (textlessSpeechBubble.LifeTime <= 0) { textlessSpeechBubble = null; } + } + if (!enabled) { return; } if (!IsIncapacitated) @@ -890,13 +975,6 @@ namespace Barotrauma pos.Y = -pos.Y; - if (speechBubbleTimer > 0.0f) - { - GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, pos - Vector2.UnitY * 5, - speechBubbleColor * Math.Min(speechBubbleTimer, 1.0f), 0.0f, - Math.Min(speechBubbleTimer, 1.0f)); - } - if (this == controlled) { if (DebugDrawInteract) @@ -921,113 +999,232 @@ namespace Barotrauma ToolBox.GradientLerp(dist, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green), width: 2); } } - return; } - - float hoverRange = 300.0f; - float fadeOutRange = 200.0f; - float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition)); - float hudInfoAlpha = - CampaignInteractionType == CampaignMode.InteractionType.None ? - MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) : - 1.0f; - - if (!GUI.DisableCharacterNames && hudInfoVisible && - (controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f) + else { - if (info != null) + + float hoverRange = 300.0f; + float fadeOutRange = 200.0f; + float cursorDist = Vector2.Distance(WorldPosition, cam.ScreenToWorld(PlayerInput.MousePosition)); + float hudInfoAlpha = + CampaignInteractionType == CampaignMode.InteractionType.None ? + MathHelper.Clamp(1.0f - (cursorDist - (hoverRange - fadeOutRange)) / fadeOutRange, 0.2f, 1.0f) : + 1.0f; + + if (!GUI.DisableCharacterNames && hudInfoVisible && + (controlled == null || this != controlled.FocusedCharacter || IsPet) && cam.Zoom > 0.4f) { - LocalizedString name = Info.DisplayName; - if (controlled == null && name != Info.Name) - { - name += " " + TextManager.Get("Disguised"); - } - else if (Info.Title != null && TeamID != CharacterTeamType.Team1) + if (info != null) { - name += '\n' + Info.Title; + LocalizedString name = Info.DisplayName; + if (controlled == null && name != Info.Name) + { + name += " " + TextManager.Get("Disguised"); + } + else if (Info.Title != null && TeamID != CharacterTeamType.Team1) + { + name += '\n' + Info.Title; + } + + Vector2 nameSize = GUIStyle.Font.MeasureString(name); + Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; + Color nameColor = GetNameColor(); + + Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); + namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; + namePos *= screenSize / viewportSize; + namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y); + namePos *= viewportSize / screenSize; + namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; + + if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) + { + var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + if (iconStyle != null) + { + Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; + Vector2 iconPos = headPos; + iconPos.Y = -iconPos.Y; + nameColor = iconStyle.Color; + var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); + float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale; + icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); + } + } + + GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); + GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); + if (GameMain.DebugDraw) + { + GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); + } } - Vector2 nameSize = GUIStyle.Font.MeasureString(name); - Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; - Color nameColor = GetNameColor(); - - Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - Vector2 viewportSize = new Vector2(cam.WorldView.Width, cam.WorldView.Height); - namePos.X -= cam.WorldView.X; namePos.Y += cam.WorldView.Y; - namePos *= screenSize / viewportSize; - namePos.X = (float)Math.Floor(namePos.X); namePos.Y = (float)Math.Floor(namePos.Y); - namePos *= viewportSize / screenSize; - namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; - - if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) + var petBehavior = (AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null && !IsDead && !IsUnconscious) { - var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + var petStatus = petBehavior.GetCurrentStatusIndicatorType(); + var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; Vector2 iconPos = headPos; iconPos.Y = -iconPos.Y; - nameColor = iconStyle.Color; var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale; + float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); } } + } - GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); - GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); - if (GameMain.DebugDraw) + if (IsDead) { return; } + + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode != EnemyHealthBarMode.ShowAll) + { + if (Controlled == null) { - GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); + if (!IsOnPlayerTeam) { return; } + } + else + { + if (!HumanAIController.IsFriendly(Controlled, this) || + (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy))) + { + return; + } } } - - var petBehavior = (AIController as EnemyAIController)?.PetBehavior; - if (petBehavior != null && !IsDead && !IsUnconscious) + + if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { - var petStatus = petBehavior.GetCurrentStatusIndicatorType(); - var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); - if (iconStyle != null) - { - Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; - Vector2 iconPos = headPos; - iconPos.Y = -iconPos.Y; - var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; - icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); - } + hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); + + Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); + GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), + CharacterHealth.DisplayedVitality / MaxVitality, + Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, + new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } } - if (IsDead) { return; } - - var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; - if (healthBarMode != EnemyHealthBarMode.ShowAll) + if (textlessSpeechBubble != null) { - if (Controlled == null) + Vector2 iconPos = pos - Vector2.UnitY * 5; + + GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, iconPos, + textlessSpeechBubble.Color * Math.Min(textlessSpeechBubble.LifeTime, 1.0f), 0.0f, + Math.Min(textlessSpeechBubble.LifeTime, 1.0f)); + } + } + + public void ShowSpeechBubble(Color color, string text) + { + if (!GameSettings.CurrentConfig.ChatSpeechBubbles) + { + ShowTextlessSpeechBubble(1.0f, color); + return; + } + float duration = MathHelper.Lerp(1.0f, 8.0f, Math.Min(text.Length / 100.0f, 1.0f)); + speechBubbles.Add(new SpeechBubble(this, duration, color, text)); + textlessSpeechBubble = null; + } + + public void ShowTextlessSpeechBubble(float duration, Color color) + { + if (speechBubbles.Any(sb => sb.Character == this)) { return; } + if (textlessSpeechBubble == null) + { + textlessSpeechBubble = new SpeechBubble(this, duration, color); + } + else + { + textlessSpeechBubble.Color = color; + textlessSpeechBubble.LifeTime = Math.Max(textlessSpeechBubble.LifeTime, duration); + } + } + + public static void DrawSpeechBubbles(SpriteBatch spriteBatch, Camera cam) + { + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); + foreach (var bubble in speechBubbles) + { + Vector2 iconPos = Timing.Interpolate(bubble.PrevPosition, bubble.Position); + iconPos += Vector2.UnitY * bubble.MoveUpAmount; + if (bubble.Submarine != null) { - if (!IsOnPlayerTeam) { return; } + iconPos += bubble.Submarine.DrawPosition; } - else + + float alpha = 1.0f; + float mouseDist = Vector2.Distance(cam.WorldToScreen(iconPos), PlayerInput.MousePosition); + //treat the size of the bubble from corner to corner as the + float textSize = bubble.TextSize.Length(); + if (mouseDist < textSize) { - if (!HumanAIController.IsFriendly(Controlled, this) || - (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy))) - { - return; + alpha *= Math.Max(mouseDist / textSize, 0.5f); + } + + iconPos.Y = -iconPos.Y; + if (GUIStyle.SpeechBubbleIconSliced.Value is { } speechBubbleIconSliced) + { + Vector2 bubbleSize = bubble.TextSize + Vector2.One * GUI.IntScale(15); + speechBubbleIconSliced.Draw(spriteBatch, new RectangleF(iconPos - bubbleSize / 2, bubbleSize), bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha); + } + GUI.DrawString(spriteBatch, iconPos - bubble.TextSize / 2, bubble.Text, bubble.Color * Math.Min(bubble.LifeTime, 1.0f) * alpha, font: GUIStyle.SmallFont); + } + spriteBatch.End(); + } + + static partial void UpdateSpeechBubbles(float deltaTime) + { + for (int i = speechBubbles.Count - 1; i >= 0; i--) + { + var bubble = speechBubbles[i]; + bubble.LifeTime -= deltaTime; + if (bubble.LifeTime <= 0 || bubble.Character is { Removed: true }) + { + speechBubbles.RemoveAt(i); + continue; + } + + bubble.PrevPosition = bubble.Position; + + Vector2 desiredPos = bubble.GetDesiredPosition(); + Vector2 diff = desiredPos - bubble.Position; + float dist = diff.Length(); + //how far the bubble needs to be from the desired position to start moving + const float MoveThreshold = 100.0f; + const float MaxSpeed = 1000.0f; + if (dist < 1) + { + bubble.Moving = false; + } + else if (dist > MoveThreshold || bubble.Moving) + { + Vector2 moveAmount = diff / dist * MathHelper.Clamp(dist * 5, 0, MaxSpeed) * deltaTime; + //slower vertical movement (don't want to interfere too much with the bubbles floating up + //and the overlap prevention which works vertically) + moveAmount.Y *= 0.1f; + bubble.Position += moveAmount; + bubble.Moving = true; + } + + bubble.MoveUpAmount += deltaTime * 5.0f; + //go through the newer bubbles, move this one out of the way if one is overlapping + for (int j = i + 1; j < speechBubbles.Count; j++) + { + var otherBubble = speechBubbles[j]; + { + if (Math.Abs(bubble.Position.X - otherBubble.Position.X) < (bubble.TextSize.X + otherBubble.TextSize.X) / 2 && + Math.Abs(bubble.Position.Y - otherBubble.Position.Y) < (bubble.TextSize.Y + otherBubble.TextSize.Y) / 2 + 10) + { + bubble.Position += Vector2.UnitY * deltaTime * 50.0f; + } } } } - - if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) - { - hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); - - Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); - GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), - CharacterHealth.DisplayedVitality / MaxVitality, - Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, - new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); - } } public Color GetNameColor() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 348cdd874..1bee8cd84 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -207,7 +207,7 @@ namespace Barotrauma { cachedHudTexts.Clear(); } - Identifier key = (textTag + keyBind).ToIdentifier(); + Identifier key = (textTag + keyBind + GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).ToIdentifier(); if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; cachedHudTexts.Add(key, text); @@ -246,6 +246,7 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { + UpdateBossProgressBars(deltaTime); if (GUI.DisableHUD) @@ -256,6 +257,11 @@ namespace Barotrauma } return; } + + if (character.ShowInteractionLabels && character.ViewTarget == null) + { + InteractionLabelManager.Update(character, cam); + } if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen) { @@ -375,7 +381,7 @@ namespace Barotrauma static bool DrawIcon(Order o) => o != null && - (!(o.TargetEntity is Item i) || + (o.TargetEntity is not Item i || o.DrawIconWhenContained || i.GetRootInventoryOwner() == i); } @@ -405,90 +411,115 @@ namespace Barotrauma if (!brokenItem.IsInteractable(character)) { continue; } float alpha = GetDistanceBasedIconAlpha(brokenItem); 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); } + if (OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder)) + { + foreach (Item deconstructItem in Item.DeconstructItems) + { + if (deconstructItem.ParentInventory != null) { continue; } + if (deconstructItem.OrderedToBeIgnored) { continue; } + if (deconstructItem.Submarine != character.Submarine || !deconstructItem.IsInteractable(character)) { continue; } + float alpha = GetDistanceBasedIconAlpha(deconstructItem, maxDistance: 450) * 0.7f; + if (alpha <= 0.0f) { continue; } + GUI.DrawIndicator(spriteBatch, deconstructItem.DrawPosition, cam, 100.0f, deconstructOrder.SymbolSprite, + GUIStyle.Red, scaleMultiplier: 0.5f, overrideAlpha: alpha); + } + } + float GetDistanceBasedIconAlpha(ISpatialEntity target, float maxDistance = 1000.0f) { float dist = Vector2.Distance(character.WorldPosition, target.WorldPosition); return Math.Min((maxDistance - dist) / maxDistance * 2.0f, 1.0f); } - + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.HeldItems.None(it => it?.GetComponent() != null))) { - if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) + if (!character.ShowInteractionLabels) { - DrawCharacterHoverTexts(spriteBatch, cam, character); - } - - if (character.FocusedItem != null) - { - if (focusedItem != character.FocusedItem) + if (character.FocusedCharacter is { CanBeSelected: true }) { - focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); - RecreateHudTexts = true; + DrawCharacterHoverTexts(spriteBatch, cam, character); } - focusedItem = character.FocusedItem; - } - - if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay) - { - Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition); - float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; - circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); - if (circleSize > 0.0f) + + if (character.FocusedItem != null) { - Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X); - GUIStyle.FocusIndicator.Draw(spriteBatch, - (int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f), - circlePos, - Color.LightBlue * 0.3f, - origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2, - rotate: (float)Timing.TotalTime, - scale: scale); - } - - if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) - { - bool hudTextsContextual = PlayerInput.IsShiftDown(); - if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual) + if (focusedItem != character.FocusedItem) { + focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); RecreateHudTexts = true; - lastHudTextsContextual = hudTextsContextual; } - var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts); - RecreateHudTexts = false; - - int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); - - Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text); - Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text); - - Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition); - startPos.Y -= (hudTexts.Count + 1) * textSize.Y; - if (focusedItem.Sprite != null) + focusedItem = character.FocusedItem; + } + + if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay) + { + Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition); + float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; + circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); + if (circleSize > 0.0f) { - startPos.X += (int)(circleSize * 0.4f * dir); - startPos.Y -= (int)(circleSize * 0.4f); + Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X); + GUIStyle.FocusIndicator.Draw(spriteBatch, + (int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f), + circlePos, + Color.LightBlue * 0.3f, + origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2, + rotate: (float)Timing.TotalTime, + scale: scale); } - Vector2 textPos = startPos; - if (dir == -1) { textPos.X -= largeTextSize.X; } - - float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f); - - GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No); - startPos.X += dir * 10.0f * GUI.Scale; - textPos.X += dir * 10.0f * GUI.Scale; - textPos.Y += largeTextSize.Y; - foreach (ColoredText coloredText in hudTexts.Skip(1)) + if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { - if (dir == -1) textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X); - GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont); - textPos.Y += textSize.Y; - } - } + bool hudTextsContextual = PlayerInput.KeyDown(InputType.ContextualCommand); + if (RecreateHudTexts || lastHudTextsContextual != hudTextsContextual) + { + RecreateHudTexts = true; + lastHudTextsContextual = hudTextsContextual; + } + var hudTexts = focusedItem.GetHUDTexts(character, RecreateHudTexts); + RecreateHudTexts = false; + + int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); + + Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text); + Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text); + + Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition); + startPos.Y -= (hudTexts.Count + 1) * textSize.Y; + if (focusedItem.Sprite != null) + { + startPos.X += (int)(circleSize * 0.4f * dir); + startPos.Y -= (int)(circleSize * 0.4f); + } + + Vector2 textPos = startPos; + if (dir == -1) { textPos.X -= largeTextSize.X; } + + float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f); + + GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No); + startPos.X += dir * 10.0f * GUI.Scale; + textPos.X += dir * 10.0f * GUI.Scale; + textPos.Y += largeTextSize.Y; + foreach (ColoredText coloredText in hudTexts.Skip(1)) + { + if (dir == -1) + { + textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X); + } + GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont); + textPos.Y += textSize.Y; + } + } + } + } + + if (character.ShowInteractionLabels && character.ViewTarget == null) + { + InteractionLabelManager.DrawLabels(spriteBatch, cam, character); } foreach (HUDProgressBar progressBar in character.HUDProgressBars.Values) @@ -673,6 +704,8 @@ namespace Barotrauma } } + + public static bool MouseOnCharacterPortrait() { if (Character.Controlled == null) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 1ce98e9b5..0b41bcf3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Linq; @@ -46,6 +46,7 @@ namespace Barotrauma ContentPath tintMaskPath = maskElement.GetAttributeContentPath("texture"); if (!tintMaskPath.IsNullOrEmpty()) { + VerifySpriteTagsLoaded(); tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this)); tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f); tintHighlightMultiplier = maskElement.GetAttributeFloat("highlightmultiplier", 0.8f); @@ -61,7 +62,7 @@ namespace Barotrauma //Stretch = true }; - var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true); + var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedFrame.RectTransform), isHorizontal: true); new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect)); @@ -185,7 +186,7 @@ namespace Barotrauma private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) { - if (_headSprite == null) { return; } + if (HeadSprite == null) { return; } Vector2 targetAreaSize = componentRect.Size.ToVector2(); float scale = Math.Min(targetAreaSize.X / _headSprite.size.X, targetAreaSize.Y / _headSprite.size.Y); DrawIcon(sb, componentRect.Location.ToVector2() + _headSprite.size / 2 * scale, targetAreaSize); @@ -537,8 +538,7 @@ namespace Barotrauma Color skinColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8(); - - string ragdollFile = inc.ReadString(); + Identifier npcId = inc.ReadIdentifier(); Identifier factionId = inc.ReadIdentifier(); @@ -560,18 +560,15 @@ namespace Barotrauma throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); } byte skillCount = inc.ReadByte(); - List jobSkills = jobPrefab?.Skills.OrderBy(s => s.Identifier).ToList(); for (int i = 0; i < skillCount; i++) { + Identifier skillIdentifier = inc.ReadIdentifier(); float skillLevel = inc.ReadSingle(); - if (jobSkills != null && i < jobSkills.Count) - { - skillLevels.Add(jobSkills[i].Identifier, skillLevel); - } + skillLevels.Add(skillIdentifier, skillLevel); } } - CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) + CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, variant, npcIdentifier: npcId) { ID = infoID, MinReputationToHire = (factionId, minReputationToHire) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index de19d3561..08f944f2c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Immutable; @@ -532,6 +533,47 @@ namespace Barotrauma bool removeOnDeath = msg.ReadBoolean(); info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); } + break; + case EventType.LatchOntoTarget: + bool attached = msg.ReadBoolean(); + if (attached) + { + Vector2 characterSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + Vector2 attachSurfaceNormal = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + Vector2 attachPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + int attachWallIndex = msg.ReadInt32(); + UInt16 attachTargetId = msg.ReadUInt16(); + + if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi }) + { + var attachTargetEntity = FindEntityByID(attachTargetId); + switch (attachTargetEntity) + { + case Character attachTargetCharacter: + latchOntoAi.SetAttachTarget(attachTargetCharacter); + break; + case Structure attachTargetStructure: + latchOntoAi.SetAttachTarget(attachTargetStructure, attachPos, attachSurfaceNormal); + break; + default: + var allLevelWalls = Level.Loaded.GetAllCells(); + if (attachWallIndex >= 0 && attachWallIndex <= allLevelWalls.Count) + { + latchOntoAi.SetAttachTarget(allLevelWalls[attachWallIndex]); + } + break; + } + latchOntoAi.AttachToBody(attachPos, attachSurfaceNormal, characterSimPos); + } + } + else + { + if (AIController is EnemyAIController { LatchOntoAI: { } latchOntoAi }) + { + latchOntoAi.DeattachFromBody(reset: false); + } + } + break; } msg.ReadPadBits(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index b0a721fc3..e9798be70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -858,8 +858,8 @@ namespace Barotrauma foreach (GUIComponent component in recommendedTreatmentContainer.Content.Children) { var treatmentButton = component.GetChild(); - if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; } - var matchingItem = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); + if (treatmentButton?.UserData is not ItemPrefab itemPrefab) { continue; } + var matchingItem = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, itemPrefab.Identifier); treatmentButton.Enabled = matchingItem != null; if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) { @@ -1387,7 +1387,6 @@ namespace Barotrauma //float = suitability Dictionary treatmentSuitability = new Dictionary(); GetSuitableTreatments(treatmentSuitability, - normalize: true, user: Character.Controlled, ignoreHiddenAfflictions: true, limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); @@ -1421,9 +1420,12 @@ namespace Barotrauma int count = 0; foreach (KeyValuePair treatment in treatmentSuitabilities) { + //don't list negative treatments + if (treatment.Value < 0) { continue; } + count++; if (count > 5) { break; } - if (!(MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) is ItemPrefab item)) { continue; } + if (MapEntityPrefab.FindByIdentifier(treatment.Key) is not ItemPrefab item) { continue; } var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 6.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft), style: null) @@ -1439,7 +1441,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (userdata is not ItemPrefab itemPrefab) { return false; } - var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); + var item = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, it => it.Prefab == itemPrefab); if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs new file mode 100644 index 000000000..73e733677 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/InteractionLabelManager.cs @@ -0,0 +1,328 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System.Collections.Generic; +using Barotrauma.Items.Components; + +namespace Barotrauma; + +public static class InteractionLabelManager +{ + + private class LabelData + { + private readonly Camera drawCamera; + public readonly Item Item; + + public RectangleF TextRect { get; set; } + + public readonly Vector2 OriginalItemPosition; + + public bool OverlapPreventionDone; + + public LabelData(Item item, RectangleF textRect, Camera drawCamera) + { + Item = item; + TextRect = textRect; + OriginalItemPosition = item.Position; + this.drawCamera = drawCamera; + } + + public RectangleF GetScreenDrawRect(Camera cam) + { + float scale = cam.Zoom; + RectangleF screenDrawRect = TextRect; + screenDrawRect.Location = drawCamera + .WorldToScreen(screenDrawRect.Location + (Item.Submarine?.DrawPosition ?? Vector2.Zero)); + + return new RectangleF( + screenDrawRect.X, + screenDrawRect.Y, + screenDrawRect.Width * scale, + screenDrawRect.Height * scale); + } + + public Vector2 GetInteractableDrawPositionScreen() + { + return drawCamera.WorldToScreen(Item.DrawPosition); + } + } + + private static readonly List labels = new(); + + private const int TextBoxMarginPx = 4; + + /// + /// Multiplier on the scale of the labels. Ad-hoc formula: since the zoom affects the size of the labels, + /// and high resolutions are more zoomed in to keep the view range the same, let's scale down the labels on large resolutions to compensate. + /// + private static float LabelScale => 1.0f / GUI.Scale; + + private static InteractionLabelDisplayMode displayMode; + + private static int graphicsWidth, graphicsHeight; + + private static bool shouldRecalculate; + private static bool recalculateEverything; + + private static readonly List interactablesInRange = new(); + + internal static Item? HoveredItem { get; private set; } + + internal static void RefreshInteractablesInRange(List interactables) + { + interactablesInRange.Clear(); + interactablesInRange.AddRange(interactables); + + shouldRecalculate = true; + } + + private static void RecalculateLabelPositions(Camera cam, Character character) + { + if (recalculateEverything) + { + labels.Clear(); + recalculateEverything = false; + } + + labels.RemoveAll(l => !interactablesInRange.Contains(l.Item)); + + // for every interactable, create a label data object with relevant info for real-time drawing + foreach (var interactableInRange in interactablesInRange) + { + // this removes the hidden vents from the list + if (interactableInRange.HasTag(Tags.HiddenItemContainer)) { continue; } + + // filter out items depending on visibility filter setting + switch (displayMode) + { + case InteractionLabelDisplayMode.InteractionAvailable when !interactableInRange.HasVisibleInteraction(character): + case InteractionLabelDisplayMode.LooseItems when !IsLooseItem(interactableInRange): + continue; + } + + RectangleF textRect = GetLabelRect(interactableInRange, cam); + + if (labels.None(l => l.Item == interactableInRange)) + { + var labelData = new LabelData(interactableInRange, textRect, cam); + labels.Add(labelData); + } + } + + PreventInteractionLabelOverlap(centerPos: character.Position); + } + + private static bool IsLooseItem(Item item) + { + bool hasActivePhysics = item.body is { Enabled: true }; + bool hasPickableComponent = item.GetComponent() != null; + return hasActivePhysics && hasPickableComponent; + } + + private static RectangleF GetLabelRect(Item item, Camera cam) + { + // create rectangle for overlap prevention + Vector2 itemTextSizeScreen = GUIStyle.SubHeadingFont.MeasureString(item.Name) * LabelScale; + Vector2 interactablePosScreen = cam.WorldToScreen(item.Position); + RectangleF textRect = new RectangleF(interactablePosScreen.X, interactablePosScreen.Y, itemTextSizeScreen.X, itemTextSizeScreen.Y); + // center the rectangle on the item + textRect.X -= textRect.Width / 2; + textRect.Y += textRect.Height / 2; + + // inflate by a bit, because the text is drawn with padding + textRect.Inflate(TextBoxMarginPx * LabelScale, TextBoxMarginPx * LabelScale); + + // the rect has screen space size, and sub-relative position + textRect.Location = cam.ScreenToWorld(textRect.Location); + return textRect; + } + + private static void PreventInteractionLabelOverlap(Vector2 centerPos) + { + //sort by distance from "centerPos": moving labels further away from the character (or whatever the center is) is preferred + labels.Sort((l1, l2) => + Vector2.DistanceSquared(l1.TextRect.Center, centerPos).CompareTo( + Vector2.DistanceSquared(l2.TextRect.Center, centerPos))); + + const float MoveStep = 10.0f; + bool intersections = true; + int iterations = 0; + int maxIterations = System.Math.Max(labels.Count * labels.Count, 100); + + while (intersections && iterations < maxIterations) + { + intersections = false; + foreach (var label in labels) + { + if (label.OverlapPreventionDone) { continue; } + foreach (var otherLabel in labels) + { + if (label == otherLabel) { continue; } + + //allow labels to overlap if there's multiple instances of the same item at (roughly) the same position + if (label.Item.Prefab == otherLabel.Item.Prefab && + Vector2.DistanceSquared(label.Item.WorldPosition, otherLabel.Item.WorldPosition) < 1.0f) + { + continue; + } + + if (!label.TextRect.Intersects(otherLabel.TextRect)) + { + continue; + } + intersections = true; + Vector2 moveAmount = Vector2.Normalize(label.TextRect.Center - centerPos) * MoveStep; + label.TextRect = new RectangleF(label.TextRect.Location + moveAmount, label.TextRect.Size); + } + if (intersections) { break; } + } + iterations++; + } + + foreach (var labelData in labels) + { + labelData.OverlapPreventionDone = true; + } + } + + private static int GetMouseHoveredLabelIndex(Camera cam) + { + for (int i = 0; i < labels.Count; i++) + { + var labelData = labels[i]; + var drawRect = labelData.GetScreenDrawRect(cam); + if (drawRect.Contains(PlayerInput.MousePosition)) + { + return i; + } + } + return -1; + } + + private static bool RefreshSettings() + { + bool settingsChanged = false; + + if (GameSettings.CurrentConfig.InteractionLabelDisplayMode != displayMode) + { + displayMode = GameSettings.CurrentConfig.InteractionLabelDisplayMode; + settingsChanged = true; + } + + if (GameMain.GraphicsWidth != graphicsWidth || GameMain.GraphicsHeight != graphicsHeight) + { + graphicsWidth = GameMain.GraphicsWidth; + graphicsHeight = GameMain.GraphicsHeight; + settingsChanged = true; + } + + return settingsChanged; + } + + internal static void Update(Character character, Camera cam) + { + if (RefreshSettings()) { shouldRecalculate = true; recalculateEverything = true; } + + if (shouldRecalculate) + { + RecalculateLabelPositions(cam, character); + } + } + + internal static void DrawLabels(SpriteBatch spriteBatch, Camera cam, Character character) + { + //if any item changes subs or moves significantly, we need to recalculate the label position + foreach (var label in labels) + { + const float MoveThreshold = 150.0f; + if (Vector2.DistanceSquared(label.OriginalItemPosition, label.Item.Position) > MoveThreshold * MoveThreshold) + { + label.TextRect = GetLabelRect(label.Item, cam); + } + } + + // find out if mouse is on top of any of the labels + int mouseOnLabelIndex = GetMouseHoveredLabelIndex(cam); + bool isMouseOnLabel = mouseOnLabelIndex >= 0; + + const float LineAlpha = 0.5f; + + if (!isMouseOnLabel) + { + HoveredItem = null; + } + + // draw order: draw lines for labels first + for (int i = 0; i < labels.Count; i++) + { + // Skip the box that the mouse is on, it will be drawn last + if (i == mouseOnLabelIndex) { continue; } + + DrawLineForLabel(spriteBatch, cam, labels[i], GUIStyle.InteractionLabelColor * LineAlpha); + } + + // Then draw labels + for (int i = 0; i < labels.Count; i++) + { + // Skip the box that the mouse is on, it will be drawn last + if (i == mouseOnLabelIndex) { continue; } + + DrawLabelForItem(spriteBatch, cam, labels[i], GUIStyle.InteractionLabelColor); + } + + // Draw the label and line that the mouse is on last (for draw order) + if (isMouseOnLabel) + { + var labelData = labels[mouseOnLabelIndex]; + + HoveredItem = labelData.Item; + + DrawLineForLabel(spriteBatch, cam, labelData, GUIStyle.InteractionLabelHoverColor * LineAlpha); + DrawLabelForItem(spriteBatch, cam,labelData, GUIStyle.InteractionLabelHoverColor); + } + } + + private static void DrawLineForLabel(SpriteBatch spriteBatch, Camera cam, LabelData labelData, Color color) + { + var drawRect = labelData.GetScreenDrawRect(cam); + // deflate by one pixel to avoid gap between line and box graphic edge + const int lineAnchorInsetPx = 1; + var deflateAmount = lineAnchorInsetPx * GUI.Scale; + deflateAmount = MathHelper.Max(deflateAmount * Screen.Selected.Cam.Zoom, lineAnchorInsetPx); + drawRect.Inflate(-deflateAmount, -deflateAmount); + + var itemDrawPosScreen = labelData.GetInteractableDrawPositionScreen(); + + // if item position is inside the box, don't draw a line + if (drawRect.Contains(itemDrawPosScreen)) { return; } + + // find the point on the box edge that is closest to the item + Vector2 textLineAnchorScreenPos = new Vector2( + MathHelper.Clamp(itemDrawPosScreen.X, drawRect.Left, drawRect.Right), + MathHelper.Clamp(itemDrawPosScreen.Y, drawRect.Top, drawRect.Bottom)); + + // draw line from label to item in the world + GUI.DrawLine(spriteBatch, textLineAnchorScreenPos, itemDrawPosScreen, color, depth: 0f, width: 2f); + } + + private static void DrawLabelForItem(SpriteBatch spriteBatch, Camera cam, LabelData labelData, Color color) + { + float scale = Screen.Selected.Cam.Zoom * LabelScale; + + var textDrawRect = labelData.GetScreenDrawRect(cam); + RectangleF backgroundRect = textDrawRect; + + // remove margin from the box the text is drawn in + textDrawRect.Inflate(-TextBoxMarginPx * scale, -TextBoxMarginPx * scale); + Vector2 textDrawPosScreen = new Vector2(textDrawRect.X, textDrawRect.Y); + + GUIStyle.InteractionLabelBackground.Draw(spriteBatch, backgroundRect, color * 0.7f); + + GUIStyle.SubHeadingFont.DrawString(spriteBatch, + labelData.Item.Name, + textDrawPosScreen, color, rotation: 0, origin: Vector2.Zero, scale, spriteEffects: SpriteEffects.None, layerDepth: 0.0f, + forceUpperCase: ForceUpperCase.No); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 32ce3df20..519b12eed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -483,9 +483,11 @@ namespace Barotrauma //2. check if the base prefab defines the texture if (texturePath.IsNullOrEmpty() && !character.Prefab.VariantOf.IsEmpty) { + Identifier speciesName = character.GetBaseCharacterSpeciesName(); RagdollParams parentRagdollParams = character.IsHumanoid ? - RagdollParams.GetRagdollParams(character.Prefab.VariantOf) : - RagdollParams.GetRagdollParams(character.Prefab.VariantOf); + RagdollParams.GetDefaultRagdollParams(speciesName, character.Params, character.Prefab.ContentPackage) : + RagdollParams.GetDefaultRagdollParams(speciesName, character.Params, character.Prefab.ContentPackage); + texturePath = parentRagdollParams.OriginalElement?.GetAttributeContentPath("texture"); } //3. "default case", get the texture from this character's XML @@ -514,8 +516,8 @@ namespace Barotrauma if (characterInfo != null) { spritePath = characterInfo.ReplaceVars(spritePath); - - if (characterInfo.HeadSprite != null && characterInfo.SpriteTags.Any()) + characterInfo.VerifySpriteTagsLoaded(); + if (characterInfo.SpriteTags.Any()) { string tags = ""; characterInfo.SpriteTags.ForEach(tag => tags += $"[{tag}]"); @@ -695,7 +697,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { var spriteParams = Params.GetSprite(); - if (spriteParams == null) { return; } + if (spriteParams == null || Alpha <= 0) { return; } float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; float brightness = Math.Max(1.0f - burn, 0.2f); Color clr = spriteParams.Color; @@ -724,6 +726,8 @@ namespace Barotrauma color = overrideColor ?? color; blankColor = overrideColor ?? blankColor; + color *= Alpha; + blankColor *= Alpha; if (isSevered) { @@ -779,7 +783,7 @@ namespace Barotrauma else { bool useTintMask = TintMask != null && spriteBatch.GetCurrentEffect() is null; - if (useTintMask) + if (useTintMask && Sprite?.Texture != null && TintMask?.Texture != null) { tintEffectParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; tintEffectParams.Params ??= new Dictionary(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs index 014ccad7b..34d62728d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxComponent.cs @@ -65,7 +65,7 @@ namespace Barotrauma bool isEditor = Screen.Selected is { IsEditor: true }; GUILayoutGroup titleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), listBox.Content.RectTransform)); - new GUITextBlock(new RectTransform(Vector2.One, titleHolder.RectTransform), Item.Name, font: GUIStyle.LargeFont) + new GUITextBlock(new RectTransform(Vector2.One, titleHolder.RectTransform), Item.Prefab.Name, font: GUIStyle.LargeFont) { TextColor = Color.White, Color = Color.Black @@ -84,7 +84,10 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); - var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont); + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame: !isEditor, showName: false, titleFont: GUIStyle.SubHeadingFont) + { + Readonly = CircuitBox.Locked + }; fieldCount += componentEditor.Fields.Count; ic.CreateEditingHUD(componentEditor); diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs new file mode 100644 index 000000000..498b61481 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxLabelNode.cs @@ -0,0 +1,184 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal sealed partial class CircuitBoxLabelNode + { + private CircuitBoxLabel headerLabel; + private readonly GUITextBlock bodyLabel; + private const string PromptUserData = "LabelEditPrompt"; + + public override void DrawHeader(SpriteBatch spriteBatch, RectangleF rect, Color color) + { + GUI.DrawString(spriteBatch, new Vector2(rect.X + CircuitBoxSizes.NodeHeaderTextPadding, rect.Center.Y - headerLabel.Size.Y / 2f), headerLabel.Value, GUIStyle.TextColorNormal, font: GUIStyle.LargeFont); + } + + public override void DrawBody(SpriteBatch spriteBatch, RectangleF rect, Color color) + { + bodyLabel.TextOffset = rect.Location - bodyLabel.Rect.Location.ToVector2() + new Vector2(CircuitBoxSizes.NodeBodyTextPadding); + bodyLabel.DrawManually(spriteBatch); + } + + public override void OnResized(RectangleF rect) + => UpdateTextSizes(rect); + + private void UpdateTextSizes(RectangleF rect) + { + var size = new Point((int)rect.Width - CircuitBoxSizes.NodeBodyTextPadding * 2, (int)rect.Height - CircuitBoxSizes.NodeBodyTextPadding * 2); + bodyLabel.RectTransform.NonScaledSize = size; + bodyLabel.Text = GetLocalizedText(BodyText); + if (bodyLabel.Font != null) + { + bodyLabel.Text = ToolBox.LimitStringHeight(bodyLabel.WrappedText.Value, bodyLabel.Font!, size.Y); + } + headerLabel = new CircuitBoxLabel(ToolBox.LimitString(GetLocalizedText(HeaderText), GUIStyle.LargeFont, size.X), GUIStyle.LargeFont); + + static LocalizedString GetLocalizedText(NetLimitedString text) => TextManager.Get(text.Value).Fallback(text.Value); + } + + public void PromptEditText(GUIComponent parent) + { + Color newColor = Color; + CircuitBox.UI?.SetMenuVisibility(false); + GUIFrame backgroundBlocker = new(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIBackgroundBlocker") + { + UserData = PromptUserData + }; + + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), backgroundBlocker.RectTransform, Anchor.Center), isHorizontal: false, childAnchor: Anchor.TopCenter); + + GUILayoutGroup colorLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), mainLayout.RectTransform)); + new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), colorLayout.RectTransform)) { IgnoreLayoutGroups = true }; + GUILayoutGroup colorArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), colorLayout.RectTransform), isHorizontal: true); + + GUIFrame labelArea = new(new RectTransform(new Vector2(1f, 0.65f), mainLayout.RectTransform, Anchor.Center)); + + GUIFrame header = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), labelArea.RectTransform, Anchor.TopLeft), style: "CircuitBoxTop"); + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.86f), labelArea.RectTransform, Anchor.BottomLeft), style: "CircuitBoxFrame"); + header.Color = frame.Color = Color; + + GUITextBox headerTextBox = new GUITextBox(new RectTransform(Vector2.One, header.RectTransform, Anchor.Center), text: HeaderText.Value, font: headerLabel.Font, style: "GUITextBoxNoStyle") + { + MaxTextLength = NetLimitedString.MaxLength, + Text = HeaderText.Value + }; + + GUITextBox bodyTextBox = new GUITextBox(new RectTransform(ToolBox.PaddingSizeParentRelative(frame.RectTransform, 0.95f), frame.RectTransform, Anchor.Center), text: BodyText.Value, font: GUIStyle.Font, style: "GUITextBoxNoStyle", textAlignment: Alignment.TopLeft, wrap: true) + { + MaxTextLength = NetLimitedString.MaxLength + }; + + bodyTextBox.OnEnterPressed += (textBox, text) => + { + int caretIndex = textBox.CaretIndex; + textBox.Text = $"{text[..caretIndex]}\n{text[caretIndex..]}"; + textBox.CaretIndex = caretIndex + 1; + + return true; + }; + + static void UpdateLabelColor(GUITextBox box) + { + bool found = TextManager.ContainsTag(box.Text); + box.TextColor = found + ? GUIStyle.Orange + : GUIStyle.TextColorNormal; + + if (found) + { + box.ToolTip = TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", TextManager.Get(box.Text)); + } + else + { + box.ToolTip = string.Empty; + } + } + + bodyTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); + headerTextBox.OnDeselected += (textBox, _) => UpdateLabelColor(textBox); + UpdateLabelColor(bodyTextBox); + UpdateLabelColor(headerTextBox); + + mainLayout.Recalculate(); + headerTextBox.ForceUpdate(); + + new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("confirm")) + { + OnClicked = (_, _) => + { + CircuitBox.RenameLabel(this, newColor, new NetLimitedString(headerTextBox.Text), new NetLimitedString(bodyTextBox.Text)); + RemoveEditPrompt(parent); + return true; + } + }; + new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), mainLayout.RectTransform), text: TextManager.Get("cancel")) + { + OnClicked = (_, _) => + { + RemoveEditPrompt(parent); + return true; + } + }; + + LocalizedString[] colorComponentLabels = + { + TextManager.Get("spriteeditor.colorcomponentr"), + TextManager.Get("spriteeditor.colorcomponentg"), + TextManager.Get("spriteeditor.colorcomponentb") + }; + for (int i = 0; i <= 2; i++) + { + var element = new GUIFrame(new RectTransform(new Vector2(0.33f, 1), colorArea.RectTransform), style: null); + + var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i], + font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + + var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int) + { + Font = GUIStyle.SubHeadingFont, + MinValueInt = 0, + MaxValueInt = 255 + }; + switch (i) + { + case 0: + colorLabel.TextColor = GUIStyle.Red; + numberInput.IntValue = Color.R; + numberInput.OnValueChanged += numInput => + { + newColor.R = (byte)numInput.IntValue; + header.Color = frame.Color = newColor; + }; + break; + case 1: + colorLabel.TextColor = GUIStyle.Green; + numberInput.IntValue = Color.G; + numberInput.OnValueChanged += numInput => + { + newColor.G = (byte)numInput.IntValue; + header.Color = frame.Color = newColor; + }; + break; + case 2: + colorLabel.TextColor = GUIStyle.Blue; + numberInput.IntValue = Color.B; + numberInput.OnValueChanged += numInput => + { + newColor.B = (byte)numInput.IntValue; + header.Color = frame.Color = newColor; + }; + break; + } + } + } + + public void RemoveEditPrompt(GUIComponent parent) + { + if (parent.FindChild(PromptUserData) is not { } promptParent) { return; } + parent.RemoveChild(promptParent); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs index 71d74c142..59981ca5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxMouseDragSnapshotHandler.cs @@ -1,5 +1,6 @@ #nullable enable +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -15,7 +16,17 @@ namespace Barotrauma /// internal sealed class CircuitBoxMouseDragSnapshotHandler { - public IEnumerable Nodes => circuitBoxUi.CircuitBox.Components.Union(circuitBoxUi.CircuitBox.InputOutputNodes); + public IEnumerable Nodes + { + get + { + var cb = circuitBoxUi.CircuitBox; + + foreach (var label in cb.Labels) { yield return label; } + foreach (var component in cb.Components) { yield return component; } + foreach (var node in cb.InputOutputNodes) { yield return node; } + } + } private IReadOnlyList Wires => circuitBoxUi.CircuitBox.Wires; @@ -29,6 +40,8 @@ namespace Barotrauma // Nodes that should be moved when dragging moveAffectedComponents = ImmutableHashSet.Empty; + public Option<(CircuitBoxResizeDirection, CircuitBoxNode)> LastResizeAffectedNode = Option.None; + public ImmutableHashSet GetLastComponentsUnderCursor() => lastNodesUnderCursor; public ImmutableHashSet GetMoveAffectedComponents() => moveAffectedComponents; @@ -45,6 +58,11 @@ namespace Barotrauma /// public bool IsWiring { get; private set; } + /// + /// If the user grabbed a side of a node and is resizing a node + /// + public bool IsResizing { get; private set; } + private Vector2 startClick = Vector2.Zero; private readonly CircuitBoxUI circuitBoxUi; @@ -147,6 +165,41 @@ namespace Barotrauma lastNodesUnderCursor = FindNodesUnderCursor(cursorPos); LastConnectorUnderCursor = FindConnectorUnderCursor(cursorPos); LastWireUnderCursor = FindWireUnderCursor(cursorPos); + LastResizeAffectedNode = FindResizeBorderUnderCursor(lastNodesUnderCursor, cursorPos); + } + + private static Option<(CircuitBoxResizeDirection, CircuitBoxNode)> FindResizeBorderUnderCursor(ImmutableHashSet nodes, Vector2 cursorPos) + { + foreach (var node in nodes) + { + if (!node.IsResizable) { continue; } + + const float borderSize = 32f; + + var rect = node.Rect; + RectangleF bottomBorder = new(rect.X, rect.Top, rect.Width, borderSize); + RectangleF rightBorder = new(rect.Right - borderSize, rect.Y, borderSize, rect.Height); + RectangleF leftBorder = new(rect.X, rect.Y, borderSize, rect.Height); + + bool hoverBottom = bottomBorder.Contains(cursorPos), + hoverRight = rightBorder.Contains(cursorPos), + hoverLeft = leftBorder.Contains(cursorPos); + + var dir = CircuitBoxResizeDirection.None; + + if (hoverBottom) { dir |= CircuitBoxResizeDirection.Down; } + if (hoverRight) { dir |= CircuitBoxResizeDirection.Right; } + if (hoverLeft) { dir |= CircuitBoxResizeDirection.Left; } + + if (dir is CircuitBoxResizeDirection.None) + { + continue; + } + + return Option.Some((dir, node)); + } + + return Option.None; } /// @@ -193,6 +246,7 @@ namespace Barotrauma startClick = Vector2.Zero; IsDragging = false; IsWiring = false; + IsResizing = false; lastNodesUnderCursor = ImmutableHashSet.Empty; } @@ -210,23 +264,34 @@ namespace Barotrauma IsDragging = false; } + if (LastResizeAffectedNode.IsNone()) + { + IsResizing = false; + } + // startClick is set to zero when the user releases the mouse button, so we should be neither dragging nor wiring in this state if (startClick == Vector2.Zero) { IsDragging = false; IsWiring = false; + IsResizing = false; return; } - bool isDragTresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; + if (circuitBoxUi.Locked) { return; } + bool isDragThresholdExceeded = Vector2.DistanceSquared(startClick, cursorPos) > dragTreshold * dragTreshold; - if (LastConnectorUnderCursor.IsNone()) + if (LastResizeAffectedNode.IsSome()) { - IsDragging |= isDragTresholdExceeded; + IsResizing |= isDragThresholdExceeded; + } + else if (LastConnectorUnderCursor.IsSome()) + { + IsWiring |= isDragThresholdExceeded; } else { - IsWiring |= isDragTresholdExceeded; + IsDragging |= isDragThresholdExceeded; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs index b470f97f9..ce877f013 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxNode.cs @@ -7,10 +7,10 @@ namespace Barotrauma { internal partial class CircuitBoxNode { - private RectangleF DrawRect, - TopDrawRect; + public RectangleF DrawRect; + private RectangleF TopDrawRect; - private void UpdateDrawRects() + protected void UpdateDrawRects() { var drawRect = new RectangleF(Position - Size / 2f, Size); drawRect.Y = -drawRect.Y; @@ -26,6 +26,8 @@ namespace Barotrauma UpdatePositions(); } + public virtual void OnResized(RectangleF drawRect) { } + public void DrawBackground(SpriteBatch spriteBatch, RectangleF drawRect, RectangleF topDrawRect, Color color) { CircuitBox.NodeFrameSprite?.Draw(spriteBatch, drawRect, color); @@ -39,6 +41,7 @@ namespace Barotrauma DrawBackground(spriteBatch, drawRect, topDrawRect, color); DrawHeader(spriteBatch, topDrawRect, color); + DrawBody(spriteBatch, drawRect, color); DrawConnectors(spriteBatch, drawPos); } @@ -52,6 +55,7 @@ namespace Barotrauma } public virtual void DrawHeader(SpriteBatch spriteBatch, RectangleF rect, Color color) { } + public virtual void DrawBody(SpriteBatch spriteBatch, RectangleF rect, Color color) { } public void DrawConnectors(SpriteBatch spriteBatch, Vector2 drawPos) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs index ee7a45761..a595e7410 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CircuitBox/CircuitBoxUI.cs @@ -35,6 +35,8 @@ namespace Barotrauma public List VirtualWires = new(); + public bool Locked => CircuitBox.Locked; + public CircuitBoxUI(CircuitBox box) { camera = new Camera @@ -47,7 +49,7 @@ namespace Barotrauma MouseSnapshotHandler = new CircuitBoxMouseDragSnapshotHandler(this); } -#region UI + #region UI public void CreateGUI(GUIFrame parent) { @@ -63,7 +65,7 @@ namespace Barotrauma spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - DrawHUD(spriteBatch); + DrawHUD(spriteBatch, component.Rect); spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; @@ -82,6 +84,8 @@ namespace Barotrauma OnClicked = (btn, userdata) => { componentMenuOpen = !componentMenuOpen; + if (Locked) { componentMenuOpen = false; } + foreach (GUIComponent child in btn.Children) { child.SpriteEffects = componentMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; @@ -139,65 +143,68 @@ namespace Barotrauma }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), parent.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, - style: "GUIButtonSettings") + style: "GUIButtonSettings") + { + OnClicked = (btn, userdata) => { - OnClicked = (btn, userdata) => - { - GUIContextMenu.CreateContextMenu( - new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera) + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("circuitboxsetting.resetview", isEnabled: true, onSelected: ResetCamera) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.resetview") + }, + new ContextMenuOption("circuitboxsetting.find", isEnabled: true, + new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOutput(CircuitBoxInputOutputNode.Type.Input)) { - Tooltip = TextManager.Get("circuitboxsettingdescription.resetview") + Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput") }, - new ContextMenuOption("circuitboxsetting.find", isEnabled: true, - new ContextMenuOption("circuitboxsetting.focusinput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Input)) - { - Tooltip = TextManager.Get("circuitboxsettingdescription.focusinput") - }, - new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOuput(CircuitBoxInputOutputNode.Type.Output)) - { - Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput") - }, - new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit) - { - Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits") - })); + new ContextMenuOption("circuitboxsetting.focusoutput", isEnabled: true, onSelected: () => FindInputOutput(CircuitBoxInputOutputNode.Type.Output)) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focusoutput") + }, + new ContextMenuOption("circuitboxsetting.focuscircuits", isEnabled: CircuitBox.Components.Any(), onSelected: FindCircuit) + { + Tooltip = TextManager.Get("circuitboxsettingdescription.focuscircuits") + })); - void ResetCamera() - { - // Vector2.One because Vector2.Zero means no value - camera.TargetPos = Vector2.One; - } - - void FindInputOuput(CircuitBoxInputOutputNode.Type type) - { - var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type); - if (input is null) { return; } - - camera.TargetPos = input.Position; - } - - void FindCircuit() - { - var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position)); - if (closestComponent is null) { return; } - - camera.TargetPos = closestComponent.Position; - } - return true; + void ResetCamera() + { + // Vector2.One because Vector2.Zero means no value + camera.TargetPos = Vector2.One; } - }; + + void FindInputOutput(CircuitBoxInputOutputNode.Type type) + { + var input = CircuitBox.InputOutputNodes.FirstOrDefault(n => n.NodeType == type); + if (input is null) { return; } + + camera.TargetPos = input.Position; + } + + void FindCircuit() + { + var closestComponent = CircuitBox.Components.MinBy(c => Vector2.DistanceSquared(c.Position, camera.Position)); + if (closestComponent is null) { return; } + + camera.TargetPos = closestComponent.Position; + } + + return true; + } + }; MouseSnapshotHandler.UpdateConnections(); // update scales of everything foreach (var node in CircuitBox.Components) { node.OnUICreated(); } + foreach (var node in CircuitBox.InputOutputNodes) { node.OnUICreated(); } + foreach (var wire in CircuitBox.Wires) { wire.Update(); } } - private string GetInventoryText() - => CircuitBox.ComponentContainer is { } container + private string GetInventoryText() => + CircuitBox.ComponentContainer is { } container ? $"{container.Inventory.AllItems.Count()}/{container.Capacity}" : "0/0"; @@ -209,7 +216,8 @@ namespace Barotrauma } if (componentList is null) { return; } - var playerInventory = CircuitBox.GetSortedCircuitBoxSortedItemsFromPlayer(Character.Controlled); + + var playerInventory = CircuitBox.GetSortedCircuitBoxItemsFromPlayer(Character.Controlled); foreach (GUIComponent child in componentList.Content.Children) { @@ -304,9 +312,9 @@ namespace Barotrauma } } -#endregion + #endregion - private void DrawHUD(SpriteBatch spriteBatch) + private void DrawHUD(SpriteBatch spriteBatch, Rectangle screenRect) { float scale = GUI.Scale / 1.5f; Vector2 offset = new Vector2(20, 40) * scale; @@ -352,6 +360,16 @@ namespace Barotrauma { n.DrawHUD(spriteBatch, camera); } + + if (Locked) + { + LocalizedString lockedText = TextManager.Get("CircuitBoxLocked") + .Fallback(TextManager.Get("ConnectionLocked")); + + Vector2 size = GUIStyle.LargeFont.MeasureString(lockedText); + Vector2 pos = new Vector2(screenRect.Center.X - size.X / 2, screenRect.Top + screenRect.Height * 0.05f); + GUI.DrawString(spriteBatch, pos, lockedText, Color.Red, Color.Black, 8, GUIStyle.LargeFont); + } } private void DrawSelection(SpriteBatch spriteBatch, Vector2 pos1, Vector2 pos2, Color color) @@ -367,6 +385,12 @@ namespace Barotrauma private static float lineWidth; public static void DrawRectangleWithBorder(SpriteBatch spriteBatch, RectangleF rect, Color fillColor, Color borderColor) + { + GUI.DrawFilledRectangle(spriteBatch, rect, fillColor); + DrawRectangleOnlyBorder(spriteBatch, rect, borderColor); + } + + private static void DrawRectangleOnlyBorder(SpriteBatch spriteBatch, RectangleF rect, Color borderColor) { Vector2 topRight = new Vector2(rect.Right, rect.Top), topLeft = new Vector2(rect.Left, rect.Top), @@ -375,8 +399,6 @@ namespace Barotrauma Vector2 offset = new Vector2(0f, lineWidth / 2f); - GUI.DrawFilledRectangle(spriteBatch, rect, fillColor); - spriteBatch.DrawLine(topRight, topLeft, borderColor, thickness: lineWidth); spriteBatch.DrawLine(topLeft - offset, bottomLeft + offset, borderColor, thickness: lineWidth); spriteBatch.DrawLine(bottomLeft, bottomRight, borderColor, thickness: lineWidth); @@ -393,6 +415,16 @@ namespace Barotrauma Vector2 mousePos = GetCursorPosition(); mousePos.Y = -mousePos.Y; + foreach (var label in CircuitBox.Labels) + { + if (label.IsSelected) + { + label.DrawSelection(spriteBatch, GetSelectionColor(label)); + } + + label.Draw(spriteBatch, label.Position, label.Color); + } + foreach (CircuitBoxWire wire in CircuitBox.Wires) { wire.Renderer.Draw(spriteBatch, GetSelectionColor(wire)); @@ -428,6 +460,7 @@ namespace Barotrauma Color color = moveable switch { CircuitBoxComponent node => node.Item.Prefab.SignalComponentColor, + CircuitBoxLabelNode label => label.Color, CircuitBoxInputOutputNode ioNode => ioNode.NodeType is CircuitBoxInputOutputNode.Type.Input ? GUIStyle.Green : GUIStyle.Red, _ => Color.White }; @@ -435,17 +468,49 @@ namespace Barotrauma } } + if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var resize)) + { + var (dir, node) = resize; + Vector2 dragOffset = MouseSnapshotHandler.GetDragAmount(GetCursorPosition()); + + var rect = node.Rect; + rect.Y = -rect.Y; + rect.Y -= rect.Height; + + if (dir.HasFlag(CircuitBoxResizeDirection.Down)) + { + rect.Height -= dragOffset.Y; + rect.Height = Math.Max(rect.Height, CircuitBoxLabelNode.MinSize.Y + CircuitBoxSizes.NodeHeaderHeight); + } + + if (dir.HasFlag(CircuitBoxResizeDirection.Right)) + { + rect.Width += dragOffset.X; + rect.Width = Math.Max(rect.Width, CircuitBoxLabelNode.MinSize.X); + } + + if (dir.HasFlag(CircuitBoxResizeDirection.Left)) + { + float oldWidth = rect.Width; + rect.Width -= dragOffset.X; + rect.Width = Math.Max(rect.Width, CircuitBoxLabelNode.MinSize.X); + + float actualResize = rect.Width - oldWidth; + rect.X -= actualResize; + } + + DrawRectangleOnlyBorder(spriteBatch, rect, GUIStyle.Yellow); + } + if (DraggedWire.TryUnwrap(out CircuitBoxWireRenderer? draggedWire)) { draggedWire.Draw(spriteBatch, GUIStyle.Yellow); } } - private Color GetSelectionColor(CircuitBoxNode node) - => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe); + private Color GetSelectionColor(CircuitBoxNode node) => GetSelectionColor(node.SelectedBy, node.IsSelectedByMe); - private Color GetSelectionColor(CircuitBoxWire wire) - => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe); + private Color GetSelectionColor(CircuitBoxWire wire) => GetSelectionColor(wire.SelectedBy, wire.IsSelectedByMe); private Color GetSelectionColor(ushort selectedBy, bool isSelectedByMe) { @@ -489,6 +554,7 @@ namespace Barotrauma { node.UpdateEditing(circuitComponent.RectTransform); } + break; } @@ -503,6 +569,7 @@ namespace Barotrauma { Character.DisableControls = true; } + camera.MoveCamera(deltaTime, allowMove: true, allowZoom: isMouseOn, allowInput: isMouseOn, followSub: false); if (camera.TargetPos != Vector2.Zero && MathUtils.NearlyEqual(camera.Position, camera.TargetPos, 0.01f)) @@ -547,7 +614,7 @@ namespace Barotrauma } else { - DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None,start, end, GUIStyle.Red, CircuitBox.WireSprite)); + DraggedWire = Option.Some(new CircuitBoxWireRenderer(Option.None, start, end, GUIStyle.Red, CircuitBox.WireSprite)); } } else @@ -562,6 +629,12 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked()) { + if (MouseSnapshotHandler.IsResizing && MouseSnapshotHandler.LastResizeAffectedNode.TryUnwrap(out var r)) + { + var (dir, node) = r; + CircuitBox.ResizeNode(node, dir, MouseSnapshotHandler.GetDragAmount(cursorPos)); + } + if (CircuitBox.HeldComponent.TryUnwrap(out ItemPrefab? prefab)) { CircuitBox.AddComponent(prefab, cursorPos); @@ -604,22 +677,32 @@ namespace Barotrauma { CircuitBox.RemoveComponents(CircuitBox.Components.Where(static node => node.IsSelectedByMe).ToArray()); CircuitBox.RemoveWires(CircuitBox.Wires.Where(static wire => wire.IsSelectedByMe).ToImmutableArray()); + CircuitBox.RemoveLabel(CircuitBox.Labels.Where(static label => label.IsSelectedByMe).ToImmutableArray()); } } if (componentMenu is { } menu && toggleMenuButton is { } button) { - componentMenuOpenState = componentMenuOpen ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f); + button.Enabled = !Locked; + componentMenuOpenState = componentMenuOpen && !Locked ? Math.Min(componentMenuOpenState + deltaTime * 5.0f, 1.0f) : Math.Max(componentMenuOpenState - deltaTime * 5.0f, 0.0f); menu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, menu.Rect.Height - 10), Vector2.Zero, componentMenuOpenState).ToPoint(); button.RectTransform.AbsoluteOffset = new Point(menu.Rect.X + ((menu.Rect.Width / 2) - (button.Rect.Width / 2)), menu.Rect.Y - button.Rect.Height); } + + if (selectedWireFrame is { } wireFrame) + { + wireFrame.Visible = !Locked; + } camera.Position = Vector2.Clamp(camera.Position, new Vector2(-CircuitBoxSizes.PlayableAreaSize / 2f), new Vector2(CircuitBoxSizes.PlayableAreaSize / 2f)); } + public void SetMenuVisibility(bool state) + => componentMenuOpen = state; + private void UpdateSelection() { if (!PlayerInput.IsAltDown() && PlayerInput.PrimaryMouseButtonDown()) @@ -662,35 +745,55 @@ namespace Barotrauma var wireSelection = CircuitBox.Wires.Where(static w => w.IsSelectedByMe).ToImmutableArray(); var nodeOption = GetTopmostNode(MouseSnapshotHandler.FindNodesUnderCursor(cursorPos)); var nodeSelection = CircuitBox.Components.Where(static n => n.IsSelectedByMe).ToImmutableArray(); + var labels = CircuitBox.Labels.Where(static l => l.IsSelectedByMe).ToImmutableArray(); - var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: wireOption.IsSome() || nodeOption is CircuitBoxComponent, () => + var option = new ContextMenuOption(TextManager.Get("delete"), isEnabled: (wireOption.IsSome() || nodeOption is CircuitBoxComponent or CircuitBoxLabelNode) && !Locked, () => { if (wireOption.TryUnwrap(out var wire)) { CircuitBox.RemoveWires(wire.IsSelected ? wireSelection : ImmutableArray.Create(wire)); } - if (nodeOption is CircuitBoxComponent node) + switch (nodeOption) { - CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node)); + case CircuitBoxComponent node: + CircuitBox.RemoveComponents(node.IsSelected ? nodeSelection : ImmutableArray.Create(node)); + break; + case CircuitBoxLabelNode label: + CircuitBox.RemoveLabel(label.IsSelected ? labels : ImmutableArray.Create(label)); + break; } }); + var editLabel = new ContextMenuOption(TextManager.Get("circuitboxeditlabel"), isEnabled: nodeOption is CircuitBoxLabelNode && !Locked, () => + { + if (nodeOption is not CircuitBoxLabelNode label || circuitComponent is null) { return; } + + label.PromptEditText(circuitComponent); + }); + + var addLabelOption = new ContextMenuOption(TextManager.Get("circuitboxaddlabel"), isEnabled: !Locked, () => + { + CircuitBox.AddLabel(cursorPos); + }); + + ContextMenuOption[] allOptions = { addLabelOption, editLabel, option }; + // show component name in the header to better indicate what is about to be deleted if (nodeOption is CircuitBoxComponent comp) { - GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, option); + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, comp.Item.Name, comp.Item.Prefab.SignalComponentColor, allOptions); return; } // also check if a wire is being deleted if (wireOption.TryUnwrap(out var foundWire)) { - GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, option); + GUIContextMenu.CreateContextMenu(PlayerInput.MousePosition, foundWire.UsedItemPrefab.Name, foundWire.Color, allOptions); return; } - GUIContextMenu.CreateContextMenu(option); + GUIContextMenu.CreateContextMenu(allOptions); } public CircuitBoxNode? GetTopmostNode(ImmutableHashSet nodes) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index f7cb968d5..0412ef274 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -2444,6 +2444,29 @@ namespace Barotrauma } })); + commands.Add(new Command("converttowreck", "", (string[] args) => + { + if (Screen.Selected is not SubEditorScreen) + { + ThrowError("The command can only be used in the submarine editor."); + return; + } + if (Submarine.MainSub == null) + { + ThrowError("Load a submarine first to convert it to a wreck."); + return; + } + if (Submarine.MainSub.Info.SubmarineElement == null) + { + ThrowError("The submarine must be saved before you can convert it to a wreck."); + return; + } + var wreckedSubmarineInfo = new SubmarineInfo(filePath: string.Empty, element: WreckConverter.ConvertToWreck(Submarine.MainSub.Info.SubmarineElement)); + wreckedSubmarineInfo.Name += "_Wrecked"; + wreckedSubmarineInfo.Type = SubmarineType.Wreck; + GameMain.SubEditorScreen.LoadSub(wreckedSubmarineInfo); + })); + commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) => { float defaultZoom = Screen.Selected.Cam.DefaultZoom; @@ -2495,6 +2518,49 @@ namespace Barotrauma } })); + commands.Add(new Command("listcontainertags", "Lists all container tags on the submarine.", (string[] args) => + { + if (Screen.Selected != GameMain.SubEditorScreen) + { + ThrowError("This command can only be used in the sub editor."); + return; + } + + HashSet allContainerTagsInTheGame = new(); + + foreach (var itemPrefab in ItemPrefab.Prefabs) + { + foreach (var pc in itemPrefab.PreferredContainers) + { + foreach (Identifier identifier in Enumerable.Union(pc.Primary, pc.Secondary)) + { + allContainerTagsInTheGame.Add(identifier); + } + } + } + + Dictionary prefab = new(); + + foreach (Item it in Item.ItemList) + { + foreach (var tag in allContainerTagsInTheGame) + { + if (it.GetTags().All(t => tag != t)) { continue; } + + prefab.TryAdd(tag, 0.0f); + prefab[tag]++; + } + } + + StringBuilder sb = new(); + foreach (var (tag, amount) in prefab.OrderByDescending(kvp => kvp.Value)) + { + sb.AppendLine($"{tag}: {amount}"); + } + + NewMessage(sb.ToString()); + }, isCheat: false)); + commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) => { //TODO: maybe do this automatically during loading when possible? @@ -3025,6 +3091,46 @@ namespace Barotrauma ContentPackageManager.EnabledPackages.ReloadCore(); })); + commands.Add(new Command("reloadpackage", "reloapackage [name]: reloads a content package.", (string[] args) => + { + if (args.Length < 1) + { + ThrowError("Please specify the name of the package to reload."); + return; + } + + if (args.Length < 2) + { + if (Screen.Selected == GameMain.GameScreen) + { + ThrowError("Reloading the package while in GameScreen may break things; to do it anyway, type 'reloadcorepackage [name] force'"); + return; + } + if (Screen.Selected == GameMain.SubEditorScreen) + { + ThrowError("Reloading the core package while in sub editor may break thingg; to do it anyway, type 'reloadcorepackage [name] force'"); + return; + } + } + + if (GameMain.NetworkMember != null) + { + ThrowError("Cannot change content packages while playing online"); + return; + } + + var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]); + if (package == null) + { + ThrowError($"Could not find the package {args[0]}!"); + return; + } + ContentPackageManager.EnabledPackages.ReloadPackage(package); + }, getValidArgs: () => new[] + { + ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray() + })); + #if WINDOWS commands.Add(new Command("startdedicatedserver", "", (string[] args) => { @@ -3405,6 +3511,29 @@ namespace Barotrauma } character.AnimController.ResetRagdoll(forceReload: true); }, isCheat: true)); + + commands.Add(new Command("loadanimation", "Loads an animation variation by name for the controlled character. The animation file has to be in the correct animations folder. Note: the changes are not saved!", (string[] args) => + { + var character = Character.Controlled; + if (character == null) + { + ThrowError("Not controlling any character!"); + return; + } + if (args.Length < 2) + { + ThrowError("Insufficient parameters: Have to pass the type of animation (Walk, Run, SwimSlow, SwimFast, or Crouch) and the filename!"); + return; + } + string type = args[0]; + if (!Enum.TryParse(type, ignoreCase: true, out AnimationType animationType)) + { + ThrowError($"Failed to parse animation type from {type}. Supported types are Walk, Run, SwimSlow, SwimFast, and Crouch!"); + return; + } + string fileName = args[1]; + character.AnimController.TryLoadAnimation(animationType, Path.GetFileNameWithoutExtension(fileName), out _, throwErrors: true); + }, isCheat: true)); commands.Add(new Command("reloadwearables", "Reloads the sprites of all limbs and wearable sprites (clothing) of the controlled character. Provide id or name if you want to target another character.", args => { @@ -3555,7 +3684,10 @@ namespace Barotrauma } try { - var subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + var subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => + //accept both the localized and the non-localized name of the sub + s.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + s.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (subInfo == null) { ThrowError($"Could not find a submarine with the name \"{args[0]}\"."); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs index 55f82a1d4..1b8b52ee0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs @@ -1,8 +1,10 @@ -using Barotrauma.Tutorials; using Segment = Barotrauma.ObjectiveManager.Segment; namespace Barotrauma; +/// +/// Checks the state of an Objective created using . +/// partial class CheckObjectiveAction : BinaryOptionAction { public enum CheckType @@ -12,10 +14,10 @@ partial class CheckObjectiveAction : BinaryOptionAction Incomplete } - [Serialize(CheckType.Completed, IsPropertySaveable.Yes)] + [Serialize(CheckType.Completed, IsPropertySaveable.Yes, description: "The objective must be in this state for the check to succeed.")] public CheckType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The identifier of the objective to check.")] public Identifier Identifier { get; set; } partial void DetermineSuccessProjSpecific(ref bool success) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs index 0929a180f..079fb7529 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -1,4 +1,3 @@ -using Barotrauma.Tutorials; using System; using System.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 3e9fdd7cb..71c382d10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -12,7 +12,7 @@ namespace Barotrauma { LocalizedString rewardText = GetRewardAmountText(sub); LocalizedString retVal; - if (rewardPerCrate.HasValue) + if (rewardPerCrate.HasValue) // If every crate has the same value { LocalizedString rewardPerCrateText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", rewardPerCrate.Value)); retVal = TextManager.GetWithVariables("missionrewardcargopercrate", @@ -21,7 +21,7 @@ namespace Barotrauma ("[maxitemcount]", maxItemCount.ToString()), ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖")); } - else + else // Crates have differing values, so only show the total value { retVal = TextManager.GetWithVariables("missionrewardcargo", ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖"), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index c244b7a22..9081d1751 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -151,10 +151,10 @@ namespace Barotrauma message = ModifyMessage(message); } - CoroutineManager.StartCoroutine(ShowMessageBoxAfterRoundSummary(header, message)); + CoroutineManager.StartCoroutine(ShowMessageBoxWhenRoundSummaryIsNotActive(header, message)); } - private IEnumerable ShowMessageBoxAfterRoundSummary(LocalizedString header, LocalizedString message) + private IEnumerable ShowMessageBoxWhenRoundSummaryIsNotActive(LocalizedString header, LocalizedString message) { while (GUIMessageBox.VisibleBox?.UserData is RoundSummary) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index be7a49430..f4af6fdcb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -8,6 +8,22 @@ namespace Barotrauma public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; + private void TryShowRetrievedMessage() + { + if (DetermineCompleted()) + { + if (!allRetrievedMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, allRetrievedMessage); } + //no need to show this again, clear it + allRetrievedMessage = string.Empty; + } + else + { + if (!partiallyRetrievedMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, partiallyRetrievedMessage); } + //no need to show this again, clear it + partiallyRetrievedMessage = string.Empty; + } + } + public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); @@ -30,6 +46,15 @@ namespace Barotrauma else { target.Item = Item.ReadSpawnData(msg); + target.Item.HighlightColor = GUIStyle.Orange; + target.Item.ExternalHighlight = true; + + ushort parentTargetId = msg.ReadUInt16(); + if (parentTargetId != Entity.NullEntityID) + { + target.OriginalContainer = Entity.FindEntityByID(parentTargetId) as Item; + } + if (target.Item == null) { throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); @@ -45,7 +70,7 @@ namespace Barotrauma target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); } - if (target.Item.body != null) + if (target.Item.body != null && target.Item.CurrentHull == null) { target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; } @@ -55,15 +80,25 @@ namespace Barotrauma public override void ClientRead(IReadMessage msg) { base.ClientRead(msg); + bool atLeastOneTargetWasRetrieved = false; int targetCount = msg.ReadByte(); for (int i = 0; i < targetCount; i++) { var state = (Target.RetrievalState)msg.ReadByte(); if (i < targets.Count) { + bool wasRetrieved = targets[i].Retrieved; targets[i].State = state; + if (!wasRetrieved && targets[i].Retrieved) + { + atLeastOneTargetWasRetrieved = true; + } } } + if (atLeastOneTargetWasRetrieved) + { + TryShowRetrievedMessage(); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 16ea7caa6..56bf2ac1d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -128,6 +128,11 @@ namespace Barotrauma { case "sprite": UISprite newSprite = new UISprite(subElement); + Rectangle sourceRect = newSprite.Sprite.SourceRect; + if ((sourceRect.Width <= 1 || sourceRect.Height <= 1) && newSprite.Tile) + { + DebugConsole.AddWarning($"Sprite \"{subElement.GetAttributeString("name", Name)}\" has a size of 1 or less which may cause performance problems.", contentPackage: element.ContentPackage); + } GUIComponent.ComponentState spriteState = GUIComponent.ComponentState.None; if (subElement.GetAttribute("state") != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 374e92d5e..d43f789b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -187,7 +187,16 @@ namespace Barotrauma Spacing = 1 }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont); + var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont); + + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), crewHeader.RectTransform, Anchor.CenterRight), string.Empty, textAlignment: Alignment.CenterRight) + { + TextGetter = () => + { + int crewSize = campaign?.CrewManager?.GetCharacterInfos()?.Count() ?? 0; + return $"{crewSize}/{CrewManager.MaxCrewSize}"; + } + }; crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 @@ -207,7 +216,7 @@ namespace Barotrauma { ClickSound = GUISoundType.ConfirmTransaction, ForceUpperCase = ForceUpperCase.Yes, - OnClicked = (b, o) => ValidateHires(PendingHires, true) + OnClicked = (b, o) => ValidateHires(PendingHires, createNetworkEvent: true) }; clearAllButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), group.RectTransform), text: TextManager.Get("campaignstore.clearall")) { @@ -647,7 +656,7 @@ namespace Barotrauma private bool AddPendingHire(CharacterInfo characterInfo, bool createNetworkMessage = true) { - if (PendingHires.Count + campaign.CrewManager.GetCharacters().Count() >= CrewManager.MaxCrewSize) + if (PendingHires.Count + campaign.CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { return false; } @@ -703,7 +712,7 @@ namespace Barotrauma validateHiresButton.Enabled = enoughMoney && HasPermission && pendingList.Content.RectTransform.Children.Any(); } - public bool ValidateHires(List hires, bool createNetworkEvent = false) + public bool ValidateHires(List hires, bool takeMoney = true, bool createNetworkEvent = false) { if (hires == null || hires.None()) { return false; } @@ -718,14 +727,16 @@ namespace Barotrauma if (nonDuplicateHires.None()) { return false; } - int total = HireManager.GetSalaryFor(nonDuplicateHires); - - if (!campaign.CanAfford(total)) { return false; } + if (takeMoney) + { + int total = HireManager.GetSalaryFor(nonDuplicateHires); + if (!campaign.CanAfford(total)) { return false; } + } bool atLeastOneHired = false; foreach (CharacterInfo ci in nonDuplicateHires) { - if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, Character.Controlled)) + if (campaign.TryHireCharacter(campaign.Map.CurrentLocation, ci, takeMoney: takeMoney)) { atLeastOneHired = true; } @@ -951,8 +962,8 @@ namespace Barotrauma CharacterInfo match = location.HireManager.AvailableCharacters.Find(info => info.GetIdentifierUsingOriginalName() == identifier); if (match != null) { - PendingHires.Add(match); AddPendingHire(match, createNetworkMessage: false); + System.Diagnostics.Debug.Assert(PendingHires.Contains(match)); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index cc59e8d2b..a9cb78909 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using Barotrauma.IO; @@ -641,19 +641,9 @@ namespace Barotrauma sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); break; } - case ItemAssemblyPrefab iPrefab: + case ItemAssemblyPrefab itemAssemblyPrefab: { - var (x, y) = PlayerInput.MousePosition; - foreach (var pair in iPrefab.DisplayEntities) - { - Rectangle dRect = pair.Item2; - dRect = new Rectangle(x: (int)(dRect.X * iPrefab.Scale + x), - y: (int)(dRect.Y * iPrefab.Scale - y), - width: (int)(dRect.Width * iPrefab.Scale), - height: (int)(dRect.Height * iPrefab.Scale)); - MapEntityPrefab prefab = MapEntityPrefab.Find("", pair.Item1); - prefab.DrawPlacing(spriteBatch, dRect, prefab.Scale * iPrefab.Scale); - } + itemAssemblyPrefab.Draw(spriteBatch, PlayerInput.MousePosition.FlipY()); break; } } @@ -716,7 +706,7 @@ namespace Barotrauma spriteBatch.Draw(backgroundSprite.Texture, area.Center.ToVector2() + pos, - null, color, 0.0f, backgroundSprite.size / 2, + backgroundSprite.SourceRect, color, 0.0f, backgroundSprite.size / 2, scale, spriteEffects, 0.0f); } @@ -1050,12 +1040,31 @@ namespace Barotrauma { return dragHandle.Dragging ? CursorState.Dragging : CursorState.Hand; } + //do not show the hover cursor when the cursor is on a listbox (on the listbox itself, not any of elements inside it!) + if (c is GUIListBox && (parent == null || parent == c)) + { + return CursorState.Default; + } // Some parent elements take priority // but not when the child is a GUIButton or GUITickBox - if (!(parent is GUIButton) && !(parent is GUIListBox) || + if (parent is not GUIButton && parent is not GUIListBox || (c is GUIButton) || (c is GUITickBox)) { - if (!c.Rect.Equals(monitorRect)) { return c.HoverCursor; } + if (!c.Rect.Equals(monitorRect)) + { + if (c is GUITickBox) + { + //tickboxes have some special logic: not all of the component is hoverable (just the box and the text area) + if (c.State is GUIComponent.ComponentState.Hover or GUIComponent.ComponentState.HoverSelected) + { + return c.HoverCursor; + } + } + else + { + return c.HoverCursor; + } + } } } @@ -2399,30 +2408,31 @@ namespace Barotrauma } iterations++; } - - static Vector2 ClampMoveAmount(Rectangle Rect, Rectangle clampTo, Vector2 moveAmount) - { - if (Rect.Y < clampTo.Y) - { - moveAmount.Y = Math.Max(moveAmount.Y, 0.0f); - } - else if (Rect.Bottom > clampTo.Bottom) - { - moveAmount.Y = Math.Min(moveAmount.Y, 0.0f); - } - if (Rect.X < clampTo.X) - { - moveAmount.X = Math.Max(moveAmount.X, 0.0f); - } - else if (Rect.Right > clampTo.Right) - { - moveAmount.X = Math.Min(moveAmount.X, 0.0f); - } - return moveAmount; - } } -#endregion + private static Vector2 ClampMoveAmount(Rectangle rect, Rectangle clampTo, Vector2 moveAmount) + { + if (rect.Y < clampTo.Y) + { + moveAmount.Y = Math.Max(moveAmount.Y, 0.0f); + } + else if (rect.Bottom > clampTo.Bottom) + { + moveAmount.Y = Math.Min(moveAmount.Y, 0.0f); + } + if (rect.X < clampTo.X) + { + moveAmount.X = Math.Max(moveAmount.X, 0.0f); + } + else if (rect.Right > clampTo.Right) + { + moveAmount.X = Math.Min(moveAmount.X, 0.0f); + } + return moveAmount; + } + + + #endregion #region Misc public static void TogglePauseMenu() @@ -2477,7 +2487,7 @@ namespace Barotrauma GameMain.GameSession.LoadPreviousSave(); }); - if (IsFriendlyOutpostLevel()) + if (IsFriendlyOutpostLevel() && !spMode.CrewDead) { CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index d26b86738..0be5b3a17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; using System.Linq; @@ -734,7 +734,7 @@ namespace Barotrauma DrawToolTip(spriteBatch, ToolTip, Rect); } - public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos, Color? textColor = null, Color? backgroundColor = null) { if (ObjectiveManager.ContentRunning) { return; } @@ -745,6 +745,8 @@ namespace Barotrauma if (toolTipBlock == null || (RichString)toolTipBlock.UserData != toolTip) { toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), toolTip, font: GUIStyle.SmallFont, wrap: true, style: "GUIToolTip"); + if (textColor != null) { toolTipBlock.TextColor = textColor.Value; } + if (backgroundColor != null) { toolTipBlock.Color = backgroundColor.Value; } toolTipBlock.RectTransform.NonScaledSize = new Point( (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); @@ -754,6 +756,15 @@ namespace Barotrauma toolTipBlock.RectTransform.AbsoluteOffset = pos.ToPoint(); toolTipBlock.SetTextPos(); + if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) + { + toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width,0); + } + if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) + { + toolTipBlock.RectTransform.AbsoluteOffset -= new Point(0, toolTipBlock.Rect.Height); + } + toolTipBlock.DrawManually(spriteBatch); } @@ -982,6 +993,24 @@ namespace Barotrauma } } + /// + /// Sets the minimum height of the transfrom to equal to the sum of the minimum heights of the children + /// (i.e. makes the element at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenMinHeight() + { + RectTransform.InheritTotalChildrenMinHeight(); + } + + /// + /// Sets the minimum height of the transfrom to equal to the sum of the heights of the children + /// (i.e. makes the element at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenHeight() + { + RectTransform.InheritTotalChildrenHeight(); + } + public static GUIComponent FromXML(ContentXElement element, RectTransform parent) { GUIComponent component = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 488f9a83d..019f31d92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -15,20 +15,73 @@ namespace Barotrauma public GUITextBox TextBox { get; private set; } + public override RichString ToolTip + { + get + { + return base.ToolTip; + } + set + { + base.ToolTip = value; + TextBox.ToolTip = value; + } + } + public GUIButton PlusButton { get; private set; } public GUIButton MinusButton { get; private set; } + public enum ButtonVisibility { Automatic, Manual, ForceVisible, ForceHidden } + private ButtonVisibility _plusMinusButtonVisibility; + /// + /// Whether or not the default +- buttons should be shown. Defaults to Automatic, + /// which enables it for all integers and for those floats that have a defined + /// range, because for these it is implicitly more obvious how to increment them. + /// + public ButtonVisibility PlusMinusButtonVisibility + { + get { return _plusMinusButtonVisibility; } + set + { + if (_plusMinusButtonVisibility != value) + { + _plusMinusButtonVisibility = value; + UpdatePlusMinusButtonVisibility(); + } + } + } + private void UpdatePlusMinusButtonVisibility() { - if (ForceShowPlusMinusButtons - || inputType == NumberType.Int - || (inputType == NumberType.Float && MinValueFloat > float.MinValue && MaxValueFloat < float.MaxValue)) + switch (PlusMinusButtonVisibility) { - ShowPlusMinusButtons(); - } - else - { - HidePlusMinusButtons(); + case ButtonVisibility.ForceHidden: + { + HidePlusMinusButtons(); + break; + } + case ButtonVisibility.ForceVisible: + { + ShowPlusMinusButtons(); + break; + } + case ButtonVisibility.Automatic: + { + if (inputType == NumberType.Int + || (inputType == NumberType.Float + && MinValueFloat > float.MinValue + && MaxValueFloat < float.MaxValue)) + { + ShowPlusMinusButtons(); + } + else + { + HidePlusMinusButtons(); + } + break; + } + case ButtonVisibility.Manual: + return; } } @@ -86,19 +139,6 @@ namespace Barotrauma } } - private bool forceShowPlusMinusButtons; - - public bool ForceShowPlusMinusButtons - { - get { return forceShowPlusMinusButtons; } - set - { - if (forceShowPlusMinusButtons == value) { return; } - forceShowPlusMinusButtons = value; - UpdatePlusMinusButtonVisibility(); - } - } - private int decimalsToDisplay = 1; public int DecimalsToDisplay { @@ -162,6 +202,17 @@ namespace Barotrauma } } } + + public bool Readonly + { + get { return TextBox.Readonly; } + set + { + TextBox.Readonly = value; + PlusButton.Enabled = !value; + MinusButton.Enabled = !value; + } + } public override GUIFont Font { @@ -189,11 +240,19 @@ namespace Barotrauma public float ValueStep; + // Enable holding to scroll through values faster private float pressedTimer; private readonly float pressedDelay = 0.5f; private bool IsPressedTimerRunning { get { return pressedTimer > 0; } } - public GUINumberInput(RectTransform rectT, NumberType inputType, string style = "", Alignment textAlignment = Alignment.Center, float? relativeButtonAreaWidth = null, bool hidePlusMinusButtons = false) : base(style, rectT) + public GUINumberInput( + RectTransform rectT, + NumberType inputType, + string style = "", + Alignment textAlignment = Alignment.Center, + float? relativeButtonAreaWidth = null, + ButtonVisibility buttonVisibility = ButtonVisibility.Automatic, + (GUIButton PlusButton, GUIButton MinusButton)? customPlusMinusButtons = null) : base(style, rectT) { LayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, rectT), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -233,9 +292,23 @@ namespace Barotrauma return true; }; - var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); - PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); - GUIStyle.Apply(PlusButton, "PlusButton", this); + if (customPlusMinusButtons.HasValue) + { + PlusButton = customPlusMinusButtons.Value.PlusButton; + MinusButton = customPlusMinusButtons.Value.MinusButton; + } + else // generate the default +- buttons + { + var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); + + PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); + GUIStyle.Apply(PlusButton, "PlusButton", this); + + MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); + GUIStyle.Apply(MinusButton, "MinusButton", this); + } + + // Set up default and custom +- buttons the same way to ensure uniform functionality PlusButton.ClickSound = GUISoundType.Increase; PlusButton.OnButtonDown += () => { @@ -255,9 +328,6 @@ namespace Barotrauma } return true; }; - - MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); - GUIStyle.Apply(MinusButton, "MinusButton", this); MinusButton.ClickSound = GUISoundType.Decrease; MinusButton.OnButtonDown += () => { @@ -278,10 +348,7 @@ namespace Barotrauma return true; }; - if (inputType != NumberType.Int || hidePlusMinusButtons) - { - HidePlusMinusButtons(); - } + PlusMinusButtonVisibility = buttonVisibility; if (inputType == NumberType.Int) { @@ -324,16 +391,16 @@ namespace Barotrauma private void HidePlusMinusButtons() { - PlusButton.Parent.Visible = false; - PlusButton.Parent.IgnoreLayoutGroups = true; + PlusButton.Parent.Visible = MinusButton.Parent.Visible = false; + PlusButton.Parent.IgnoreLayoutGroups = MinusButton.Parent.IgnoreLayoutGroups = true; TextBox.RectTransform.RelativeSize = Vector2.One; LayoutGroup.Recalculate(); } private void ShowPlusMinusButtons() { - PlusButton.Parent.Visible = true; - PlusButton.Parent.IgnoreLayoutGroups = false; + PlusButton.Parent.Visible = MinusButton.Parent.Visible = true; + PlusButton.Parent.IgnoreLayoutGroups = MinusButton.Parent.IgnoreLayoutGroups = false; TextBox.RectTransform.RelativeSize = new Vector2(1.0f - PlusButton.Parent.RectTransform.RelativeSize.X, 1.0f); LayoutGroup.Recalculate(); } @@ -427,6 +494,11 @@ namespace Barotrauma Math.Min(floatValue, MaxValueFloat.Value); PlusButton.Enabled = WrapAround || floatValue < MaxValueFloat; } + + if (Readonly) + { + PlusButton.Enabled = MinusButton.Enabled = false; + } } private void ClampIntValue() @@ -441,8 +513,16 @@ namespace Barotrauma intValue = WrapAround && MinValueInt.HasValue ? MinValueInt.Value : Math.Min(intValue, MaxValueInt.Value); UpdateText(); } - PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt; - MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt; + + if (Readonly) + { + PlusButton.Enabled = MinusButton.Enabled = false; + } + else + { + PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt; + MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt; + } } private void UpdateText() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 3080c482e..2486e7833 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -135,7 +135,8 @@ namespace Barotrauma if (subElement.NameAsIdentifier() != "override") { continue; } if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag)) { - return new ScalableFont(subElement, font?.Size ?? 14, GameMain.Instance.GraphicsDevice); + uint overrideFontSize = GetFontSize(subElement, defaultSize: font?.Size ?? 14); + return new ScalableFont(subElement, overrideFontSize, GameMain.Instance.GraphicsDevice); } } @@ -380,6 +381,11 @@ namespace Barotrauma public static implicit operator UISprite?(GUISprite reference) => reference.Value; + public void Draw(SpriteBatch spriteBatch, RectangleF rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) + { + Value?.Draw(spriteBatch, rect, color, spriteEffects); + } + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) { Value?.Draw(spriteBatch, rect, color, spriteEffects); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index 30ec4af6c..27bb90853 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -214,7 +214,7 @@ namespace Barotrauma Bar.HoverCursor = CursorState.Default; break; case "GUISlider": - HoverCursor = CursorState.Default; + HoverCursor = CursorState.Hand; Bar.HoverCursor = CursorState.Hand; break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs new file mode 100644 index 000000000..069c50c80 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUISelectionCarousel.cs @@ -0,0 +1,191 @@ +#nullable enable + +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + /// + /// Allows accessing the element selected in the carousel in contexts where the type of T isn't known. + /// Pretty hacky, but I could not think of a better way to do this ( in which this is used). + /// + public interface IGUISelectionCarouselAccessor + { + object? GetSelectedElement(); + void SelectElement(object? value); + } + + /// + /// An UI element that allows toggling through a set of options with buttons to the left and right + /// + public class GUISelectionCarousel : GUIComponent, IGUISelectionCarouselAccessor + { + public record class Element(T value, LocalizedString text, LocalizedString toolTip); + + public delegate void OnValueChangedHandler(GUISelectionCarousel carousel); + public OnValueChangedHandler? OnValueChanged; + + public GUITextBlock TextBlock { get; private set; } + + public GUIButton RightButton { get; private set; } + public GUIButton LeftButton { get; private set; } + + private readonly List elements = new List(); + + private readonly GUILayoutGroup layoutGroup; + + public Element? SelectedElement { get; private set; } + public T? SelectedValue => SelectedElement == null ? default : SelectedElement.value; + public LocalizedString SelectedText => SelectedElement?.text ?? string.Empty; + + public override bool Enabled + { + get => base.Enabled; + set + { + base.Enabled = RightButton.Enabled = LeftButton.Enabled = TextBlock.Enabled = value; + } + } + + + public override Color Color + { + get { return color; } + set + { + color = value; + TextBlock.Color = color; + } + } + + public Color TextColor + { + get { return TextBlock.TextColor; } + set { TextBlock.TextColor = value; } + } + + public override Color HoverColor + { + get => base.HoverColor; + set + { + base.HoverColor = value; + TextBlock.HoverColor = value; + } + } + + public GUISelectionCarousel(RectTransform rectT, string style = "", params (T value, LocalizedString text)[] newElements) : base(style, rectT) + { + layoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, rectT), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + LeftButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleLeft"); + GUIStyle.Apply(LeftButton, "LeftButton", this); + TextBlock = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), layoutGroup.RectTransform), "", textAlignment: Alignment.Center, style: "GUITextBox"); + GUIStyle.Apply(TextBlock, "TextBlock", this); + RightButton = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), layoutGroup.RectTransform), style: "GUIButtonToggleRight"); + GUIStyle.Apply(RightButton, "RightButton", this); + + RightButton.OnClicked += (btn, userData) => + { + if (elements.Count < 2) { return false; } + if (SelectedElement == null) + { + SelectElement(elements.First()); + } + else + { + int newIndex = (elements.IndexOf(SelectedElement) + 1) % elements.Count; + SelectElement(elements[newIndex]); + } + return true; + }; + LeftButton.OnClicked += (btn, userData) => + { + if (elements.Count < 2) { return false; } + if (SelectedElement == null) + { + SelectElement(elements.First()); + } + else + { + int newIndex = MathUtils.PositiveModulo((elements.IndexOf(SelectedElement) - 1), elements.Count); + SelectElement(elements[newIndex]); + } + return true; + }; + + if (newElements != null && newElements.Any()) + { + SetElements(newElements); + } + } + + public object? GetSelectedElement() + { + return SelectedValue; + } + + /// + /// Select the element whose value matches the specified value. If null, deselects the currently selected element. + /// + public void SelectElement(object? value) + { + if (value == null) + { + SelectElement(null); + return; + } + if (elements.FirstOrDefault(e => value.Equals(e.value)) is { } element) + { + SelectElement(element); + } + } + + public void SelectElement(Element? element) + { + SelectedElement = element; + TextBlock.Text = element?.text ?? string.Empty; + TextBlock.ToolTip = element?.toolTip ?? string.Empty; + OnValueChanged?.Invoke(this); + } + + /// + /// Clears all existing elements from the carousels and adds the specified new elements to it + /// + public void SetElements(params (T value, LocalizedString text)[] elements) + { + this.elements.Clear(); + foreach ((T value, LocalizedString text) in elements) + { + AddElement(value, text); + } + } + + /// + /// Clears all existing elements from the carousels and adds the specified new elements to it + /// + public void SetElements(params (T value, LocalizedString text, LocalizedString toolTip)[] elements) + { + this.elements.Clear(); + foreach ((T value, LocalizedString text, LocalizedString toolTip) in elements) + { + AddElement(value, text, toolTip); + } + } + + public void AddElement(T value, LocalizedString text, LocalizedString? tooltip = null) + { + var newElement = new Element(value, text, tooltip ?? string.Empty); + elements.Add(newElement); + if (SelectedElement == null) + { + SelectElement(newElement); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index f6e5612d9..ffb851a97 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -1,6 +1,6 @@ -using System; -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -47,6 +47,8 @@ namespace Barotrauma public readonly static GUISprite SubmarineLocationIcon = new GUISprite("SubmarineLocationIcon"); public readonly static GUISprite Arrow = new GUISprite("Arrow"); public readonly static GUISprite SpeechBubbleIcon = new GUISprite("SpeechBubbleIcon"); + public readonly static GUISprite SpeechBubbleIconSliced = new GUISprite("SpeechBubbleIconSliced"); + public readonly static GUISprite InteractionLabelBackground = new GUISprite("InteractionLabelBackground"); public readonly static GUISprite BrokenIcon = new GUISprite("BrokenIcon"); public readonly static GUISprite YouAreHereCircle = new GUISprite("YouAreHereCircle"); @@ -124,6 +126,9 @@ namespace Barotrauma public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral", new Color(228, 217, 167, 255)); public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh", new Color(51, 152, 64, 255)); public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh", new Color(71, 160, 164, 255)); + + public readonly static GUIColor InteractionLabelColor = new GUIColor("InteractionLabelColor", new Color(255, 255, 255, 255)); + public readonly static GUIColor InteractionLabelHoverColor = new GUIColor("InteractionLabelHoverColor", new Color(0, 255, 255, 255)); // Inventory public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor", new Color(99, 70, 64, 255)); @@ -195,8 +200,7 @@ namespace Barotrauma if (parentStyle == null) { - Identifier parentStyleName = parent.GetType().Name.ToIdentifier(); - + Identifier parentStyleName = ReflectionUtils.GetTypeNameWithoutGenericArity(parent.GetType()); if (!ComponentStyles.ContainsKey(parentStyleName)) { DebugConsole.ThrowError($"Couldn't find a GUI style \"{parentStyleName}\""); @@ -204,7 +208,7 @@ namespace Barotrauma } parentStyle = ComponentStyles[parentStyleName]; } - Identifier childStyleName = styleName.IsEmpty ? targetComponent.GetType().Name.ToIdentifier() : styleName; + Identifier childStyleName = styleName.IsEmpty ? ReflectionUtils.GetTypeNameWithoutGenericArity(targetComponent.GetType()) : styleName; parentStyle.ChildStyles.TryGetValue(childStyleName, out componentStyle); } else @@ -212,7 +216,7 @@ namespace Barotrauma Identifier styleIdentifier = styleName.ToIdentifier(); if (styleIdentifier == Identifier.Empty) { - styleIdentifier = targetComponent.GetType().Name.ToIdentifier(); + styleIdentifier = ReflectionUtils.GetTypeNameWithoutGenericArity(targetComponent.GetType()); } if (!ComponentStyles.ContainsKey(styleIdentifier)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 534ebdf89..b77ff8093 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -153,6 +153,9 @@ namespace Barotrauma } } + /// + /// When enabled, clips the left side of the text if it's too long to fit in the box (i.e. allows you to enter longer texts without the text overflowing from the box). + /// public bool OverflowClip { get { return textBlock.OverflowClip; } @@ -335,7 +338,7 @@ namespace Barotrauma textBlock.Text = text; ClearSelection(); if (Text == null) textBlock.Text = ""; - if (Text != "" && !Wrap) + if (Text != "") { if (maxTextLength != null) { @@ -344,7 +347,7 @@ namespace Barotrauma textBlock.Text = Text.Substring(0, (int)maxTextLength); } } - else + else if (!Wrap) { while (ClampText && textBlock.Text.Length > 0 && Font.MeasureString(textBlock.Text).X * TextBlock.TextScale > (int)(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index cc7359b59..a1a5f5b47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; namespace Barotrauma @@ -198,7 +198,9 @@ namespace Barotrauma base.Update(deltaTime); - if (GUI.MouseOn == this && Enabled) + if (GUI.MouseOn == this && Enabled && + //allow clicking on the text area, but not further to the right (the dimensions of the component itself can extend further than the text) + PlayerInput.MousePosition.X < Rect.X + ContentWidth) { State = Selected ? ComponentState.HoverSelected : diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 06a9b05a4..c92b557c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -779,6 +779,24 @@ namespace Barotrauma NonScaledSize = targetSize; yield return CoroutineStatus.Success; } + + /// + /// Sets the minimum height of the transfrom to equal to the sum of the minimum heights of the children + /// (i.e. makes the rect at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenMinHeight() + { + MinSize = new Point(MinSize.X, children.Sum(c => c.MinSize.Y)); + } + + /// + /// Sets the minimum height of the transfrom to equal to the sum of the heights of the children + /// (i.e. makes the rect at least large enough to fit all the children vertically) + /// + public void InheritTotalChildrenHeight() + { + MinSize = new Point(MinSize.X, children.Sum(c => c.Rect.Height)); + } #endregion #region Static methods diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 5264b13b5..daceed791 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -1733,6 +1733,9 @@ namespace Barotrauma if (!subItem.Components.All(c => c is not Holdable h || !h.Attachable || !h.Attached)) { continue; } if (!subItem.Components.All(c => c is not Wire w || w.Connections.All(c => c == null))) { continue; } if (!ItemAndAllContainersInteractable(subItem)) { continue; } + //don't list items in a character inventory (the ones in a crew member's inventory are counted below) + var rootInventoryOwner = subItem.GetRootInventoryOwner(); + if (rootInventoryOwner != null) { continue; } AddOwnedItem(subItem); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 4e78f979b..b132d18f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1066,7 +1066,7 @@ namespace Barotrauma GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(1f), centerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight, anchor: Anchor.Center), style: "GUIButtonTransferArrow"); GUILayoutGroup inputLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center); - GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, hidePlusMinusButtons: true) + GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden) { MinValueInt = 0 }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 6f440255e..aa8ab3f3a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -131,7 +131,7 @@ namespace Barotrauma 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); + GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter)); GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), @@ -423,7 +423,7 @@ namespace Barotrauma 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" + ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)), + ToolTip = CreateTooltip(talent, characterInfo), UserData = talent.Identifier, PressedColor = pressedColor, Enabled = info.Character != null, @@ -489,6 +489,24 @@ namespace Barotrauma }, }; + static RichString CreateTooltip(TalentPrefab talent, CharacterInfo? character) + { + LocalizedString progress = string.Empty; + + if (character is not null && talent.TrackedStat.TryUnwrap(out var stat)) + { + var statValue = character.GetSavedStatValue(StatTypes.None, stat.PermanentStatIdentifier); + var intValue = (int)MathF.Round(statValue); + progress = "\n\n"; + progress += statValue < stat.Max + ? TextManager.GetWithVariables("talentprogress", ("[amount]", intValue.ToString()), ("[max]", stat.Max.ToString())) + : TextManager.Get("talentprogresscompleted"); + } + + RichString tooltip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖\n\n{ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)}{progress}"); + return tooltip; + } + talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; GUIComponent iconImage; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3a8e2cb68..192fbe7d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1305,7 +1305,7 @@ namespace Barotrauma const int maxUpgrades = 4; Item? item = entity as Item; - itemName.Text = item?.Name ?? TextManager.Get("upgradecategory.walls"); + itemName.Text = item?.Prefab.Name ?? TextManager.Get("upgradecategory.walls"); if (slotIndex > -1) { itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs index a7a5216d2..b1d25586c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs @@ -7,46 +7,46 @@ using Barotrauma.Extensions; namespace Barotrauma { - class Widget + public enum WidgetShape { - public enum Shape - { - Rectangle, - Circle, - Cross - } + Rectangle, + Circle, + Cross + } - public Shape shape; - public LocalizedString tooltip; - public bool showTooltip = true; - public Rectangle DrawRect => new Rectangle((int)(DrawPos.X - (float)size / 2), (int)(DrawPos.Y - (float)size / 2), size, size); + internal class Widget + { + public WidgetShape Shape; + public LocalizedString Tooltip; + public bool ShowTooltip = true; + public Rectangle DrawRect => new Rectangle((int)(DrawPos.X - (float)Size / 2), (int)(DrawPos.Y - (float)Size / 2), Size, Size); public Rectangle InputRect { get { var inputRect = DrawRect; - inputRect.Inflate(inputAreaMargin, inputAreaMargin); + inputRect.Inflate(InputAreaMargin, InputAreaMargin); return inputRect; } } public Vector2 DrawPos { get; set; } - public int size = 10; - public float thickness = 1f; + public int Size = 10; + public float Thickness = 1f; /// /// Used only for circles. /// - public int sides = 40; + public int Sides = 40; /// /// Currently used only for rectangles. /// - public bool isFilled; - public int inputAreaMargin; - public Color color = GUIStyle.Red; - public Color? secondaryColor; - public Color textColor = Color.White; - public Color textBackgroundColor = Color.Black * 0.5f; - public readonly string id; + public bool IsFilled; + public int InputAreaMargin; + public Color Color = GUIStyle.Red; + public Color? SecondaryColor; + public Color TextColor = Color.White; + public Color TextBackgroundColor = Color.Black * 0.5f; + public readonly string Id; public event Action Selected; public event Action Deselected; @@ -61,11 +61,11 @@ namespace Barotrauma public bool RequireMouseOn = true; - public Action refresh; + public Action Refresh; - public object data; + public object Data; - public bool IsSelected => enabled && selectedWidgets.Contains(this); + public bool IsSelected => enabled && SelectedWidgets.Contains(this); public bool IsControlled => IsSelected && PlayerInput.PrimaryMouseButtonHeld(); public bool IsMouseOver => GUI.MouseOn == null && InputRect.Contains(PlayerInput.MousePosition); private bool enabled = true; @@ -75,9 +75,9 @@ namespace Barotrauma set { enabled = value; - if (!enabled && selectedWidgets.Contains(this)) + if (!enabled && SelectedWidgets.Contains(this)) { - selectedWidgets.Remove(this); + SelectedWidgets.Remove(this); } } } @@ -89,46 +89,46 @@ namespace Barotrauma set { multiselect = value; - if (!multiselect && selectedWidgets.Multiple()) + if (!multiselect && SelectedWidgets.Multiple()) { - selectedWidgets = selectedWidgets.Take(1).ToList(); + SelectedWidgets = SelectedWidgets.Take(1).ToList(); } } } - public Vector2? tooltipOffset; + public Vector2? TooltipOffset; - public Widget linkedWidget; + public Widget LinkedWidget; - public static List selectedWidgets = new List(); + public static List SelectedWidgets = new List(); - public Widget(string id, int size, Shape shape) + public Widget(string id, int size, WidgetShape shape) { - this.id = id; - this.size = size; - this.shape = shape; + Id = id; + Size = size; + Shape = shape; } public virtual void Update(float deltaTime) { PreUpdate?.Invoke(deltaTime); if (!enabled) { return; } - if (IsMouseOver || (!RequireMouseOn && selectedWidgets.Contains(this) && PlayerInput.PrimaryMouseButtonHeld())) + if (IsMouseOver || (!RequireMouseOn && SelectedWidgets.Contains(this) && PlayerInput.PrimaryMouseButtonHeld())) { Hovered?.Invoke(); System.Diagnostics.Debug.WriteLine("hovered"); if (RequireMouseOn || PlayerInput.PrimaryMouseButtonDown()) { - if ((multiselect && !selectedWidgets.Contains(this)) || selectedWidgets.None()) + if ((multiselect && !SelectedWidgets.Contains(this)) || SelectedWidgets.None()) { - selectedWidgets.Add(this); + SelectedWidgets.Add(this); Selected?.Invoke(); } } } - else if (selectedWidgets.Contains(this)) + else if (SelectedWidgets.Contains(this)) { System.Diagnostics.Debug.WriteLine("selectedWidgets.Contains(this) -> remove"); - selectedWidgets.Remove(this); + SelectedWidgets.Remove(this); Deselected?.Invoke(); } if (IsSelected) @@ -153,40 +153,40 @@ namespace Barotrauma { PreDraw?.Invoke(spriteBatch, deltaTime); var drawRect = DrawRect; - switch (shape) + switch (Shape) { - case Shape.Rectangle: - if (secondaryColor.HasValue) + case WidgetShape.Rectangle: + if (SecondaryColor.HasValue) { - GUI.DrawRectangle(spriteBatch, drawRect, secondaryColor.Value, isFilled, thickness: 2); + GUI.DrawRectangle(spriteBatch, drawRect, SecondaryColor.Value, IsFilled, thickness: 2); } - GUI.DrawRectangle(spriteBatch, drawRect, color, isFilled, thickness: IsSelected ? (int)(thickness * 3) : (int)thickness); + GUI.DrawRectangle(spriteBatch, drawRect, Color, IsFilled, thickness: IsSelected ? (int)(Thickness * 3) : (int)Thickness); break; - case Shape.Circle: - if (secondaryColor.HasValue) + case WidgetShape.Circle: + if (SecondaryColor.HasValue) { - ShapeExtensions.DrawCircle(spriteBatch, DrawPos, size / 2, sides, secondaryColor.Value, thickness: 2); + ShapeExtensions.DrawCircle(spriteBatch, DrawPos, Size / 2, Sides, SecondaryColor.Value, thickness: 2); } - ShapeExtensions.DrawCircle(spriteBatch, DrawPos, size / 2, sides, color, thickness: IsSelected ? 3 : 1); + ShapeExtensions.DrawCircle(spriteBatch, DrawPos, Size / 2, Sides, Color, thickness: IsSelected ? 3 : 1); break; - case Shape.Cross: - float halfSize = size / 2; - if (secondaryColor.HasValue) + case WidgetShape.Cross: + float halfSize = Size / 2; + if (SecondaryColor.HasValue) { - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, secondaryColor.Value, width: 2); - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, secondaryColor.Value, width: 2); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, SecondaryColor.Value, width: 2); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, SecondaryColor.Value, width: 2); } - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, color, width: IsSelected ? 3 : 1); - GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, color, width: IsSelected ? 3 : 1); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitY * halfSize, DrawPos - Vector2.UnitY * halfSize, Color, width: IsSelected ? 3 : 1); + GUI.DrawLine(spriteBatch, DrawPos + Vector2.UnitX * halfSize, DrawPos - Vector2.UnitX * halfSize, Color, width: IsSelected ? 3 : 1); break; - default: throw new NotImplementedException(shape.ToString()); + default: throw new NotImplementedException(Shape.ToString()); } if (IsSelected) { - if (showTooltip && !tooltip.IsNullOrEmpty()) + if (ShowTooltip && !Tooltip.IsNullOrEmpty()) { - var offset = tooltipOffset ?? new Vector2(size, -size / 2f); - GUI.DrawString(spriteBatch, DrawPos + offset, tooltip, textColor, textBackgroundColor); + var offset = TooltipOffset ?? new Vector2(Size, -Size / 2f); + GUIComponent.DrawToolTip(spriteBatch, Tooltip, DrawPos + offset, TextColor, TextBackgroundColor); } } PostDraw?.Invoke(spriteBatch, deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index d61195c58..50f3eec20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -132,7 +132,7 @@ namespace Barotrauma /// public event Action ResolutionChanged; - private bool exiting; + public static bool IsExiting { get; private set; } public static bool IsFirstLaunch { @@ -176,7 +176,7 @@ namespace Barotrauma { try { - return Instance != null && !Instance.exiting && Instance.IsActive; + return Instance != null && !IsExiting && Instance.IsActive; } catch (NullReferenceException) { @@ -465,7 +465,15 @@ namespace Barotrauma { Thread.Sleep((int)(Timing.Step * 1000)); } + LanguageIdentifier selectedLanguage = GameSettings.CurrentConfig.Language; + //unload text files at this point - we only loaded for the purposes of the language selection screen, + //they will be loaded "normally" with the rest of the files later ContentPackageManager.VanillaCorePackage.UnloadFilesOfType(); + //the selected language got unloaded, need to reselect it + var config = GameSettings.CurrentConfig; + config.Language = selectedLanguage; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); } SoundManager = new Sounds.SoundManager(); @@ -488,8 +496,10 @@ namespace Barotrauma .Select(p => p.Result).Successes()) { const float min = 1f, max = 70f; + if (IsExiting) { break; } TitleScreen.LoadState = MathHelper.Lerp(min, max, progress); } + if (IsExiting) { return; } var corePackage = ContentPackageManager.EnabledPackages.Core; if (corePackage.EnableError.TryUnwrap(out var error)) @@ -581,6 +591,8 @@ namespace Barotrauma MainMenuScreen.Select(); + ContainerTagPrefab.CheckForContainerTagErrors(); + foreach (Identifier steamError in SteamManager.InitializationErrors) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get(steamError)); @@ -1217,7 +1229,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), linkHolder.RectTransform), TextManager.Get("bugreportgithubform"), style: "MainMenuGUIButton", textAlignment: Alignment.Left) { - UserData = "https://github.com/Regalis11/Barotrauma/issues/new/choose", + UserData = "https://github.com/FakeFishGames/Barotrauma/discussions/new?category=bug-reports", OnClicked = (btn, userdata) => { ShowOpenUriPrompt(userdata as string); @@ -1241,7 +1253,7 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { - exiting = true; + IsExiting = true; CreatureMetrics.Save(); DebugConsole.NewMessage("Exiting..."); Client?.Quit(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index b8966494d..5f5862e60 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -157,6 +157,7 @@ namespace Barotrauma Character.Controlled.Info.Name, msg, messageType, Character.Controlled); + Character.Controlled.ShowSpeechBubble(ChatMessage.MessageColor[(int)messageType], text); if (messageType == ChatMessageType.Radio && headset != null) { Signal s = new Signal(msg, sender: Character.Controlled, source: headset.Item); @@ -620,7 +621,7 @@ namespace Barotrauma private void OnCrewListRearranged(GUIListBox crewList, object draggedElementData) { if (crewList != this.crewList) { return; } - if (!(draggedElementData is Character)) { return; } + if (draggedElementData is not Character) { return; } if (!IsSinglePlayer) { return; } if (crewList.HasDraggedElementIndexChanged) { @@ -645,7 +646,7 @@ namespace Barotrauma for (int i = 0; i < crewList.Content.CountChildren; i++) { var characterComponent = crewList.Content.GetChild(i); - if (!(characterComponent?.UserData is Character c)) { continue; } + if (characterComponent?.UserData is not Character c) { continue; } if (c.Info == null) { continue; } c.Info.CrewListIndex = i; } @@ -681,6 +682,7 @@ namespace Barotrauma { AddSinglePlayerChatMessage(senderName.Value, text.Value, messageType, sender); } + public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Character sender) { if (!IsSinglePlayer) @@ -798,12 +800,27 @@ namespace Barotrauma hull ??= order.OrderGiver.CurrentHull; AddOrder(order.WithTargetEntity(hull), order.FadeOutTime); } + if (order.IsDeconstructOrder) + { + if (order.TargetEntity is Item item) + { + if (order.Identifier == Tags.DeconstructThis) + { + Item.DeconstructItems.Add(item); + HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); + } + else + { + Item.DeconstructItems.Remove(item); + } + } + } else if (order.IsIgnoreOrder) { WallSection ws = null; if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is IIgnorable ignorable) { - ignorable.OrderedToBeIgnored = order.Identifier == "ignorethis"; + ignorable.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; AddOrder(order.Clone(), null); } else if (order.TargetType == Order.OrderTargetType.WallSection && order.TargetEntity is Structure s) @@ -812,7 +829,7 @@ namespace Barotrauma ws = s.GetSection(wallSectionIndex); if (ws != null) { - ws.OrderedToBeIgnored = order.Identifier == "ignorethis"; + ws.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; AddOrder(order.WithWallSection(s, wallSectionIndex), null); } } @@ -835,7 +852,9 @@ namespace Barotrauma } if (IsSinglePlayer) { - order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); + order.OrderGiver?.Speak( + order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), + ChatMessageType.Order); } else { @@ -1404,9 +1423,10 @@ namespace Barotrauma if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || (GUI.KeyboardDispatcher.Subscriber is GUIComponent component && (component == crewList || component.IsChildOf(crewList)))) && commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false) && - Character.Controlled?.SelectedItem?.Prefab is not { DisableCommandMenuWhenSelected: true }) + Character.Controlled?.SelectedItem?.Prefab is not { DisableCommandMenuWhenSelected: true } && + !Inventory.IsMouseOnInventory) { - if (PlayerInput.IsShiftDown()) + if (PlayerInput.KeyDown(InputType.ContextualCommand)) { CreateCommandUI(FindEntityContext(), true); } @@ -1535,14 +1555,14 @@ namespace Barotrauma { if (node.Keys != Keys.None && PlayerInput.KeyHit(node.Keys)) { - var b = node.Button as GUIButton; - if (PlayerInput.IsShiftDown() && b?.OnSecondaryClicked != null) + var button = node.Button; + if (PlayerInput.IsShiftDown() && button?.OnSecondaryClicked != null) { - b.OnSecondaryClicked.Invoke(node.Button as GUIButton, node.Button.UserData); + button.OnSecondaryClicked.Invoke(button, button.UserData); } else { - b?.OnClicked?.Invoke(node.Button as GUIButton, node.Button.UserData); + button?.OnClicked?.Invoke(button, button.UserData); } ResetNodeSelection(); hotkeyHit = true; @@ -1612,7 +1632,7 @@ namespace Barotrauma { foreach (var orderIcon in currentOrderIconList.Content.Children) { - if (!(orderIcon.UserData is Order order)) { continue; } + if (orderIcon.UserData is not Order order) { continue; } if (order.ColoredWhenControllingGiver && order.OrderGiver != Character.Controlled) { orderIcon.Color = AIObjective.ObjectiveIconColor; @@ -1701,7 +1721,7 @@ namespace Barotrauma bool foundMatch = false; foreach (var orderIcon in currentOrderIconList.Content.Children) { - if (!(orderIcon.GetChildByUserData("glow") is GUIComponent glowComponent)) { continue; } + if (orderIcon.GetChildByUserData("glow") is not GUIComponent glowComponent) { continue; } glowComponent.Color = orderIcon.Color; if (foundMatch) { @@ -1946,6 +1966,13 @@ namespace Barotrauma } } + public void OpenCommandUI(Entity entityContext = null, bool forceContextual = false) + { + CreateCommandUI(entityContext, forceContextual); + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); + clicklessSelectionActive = isOpeningClick = true; + } + private void CreateCommandUI(Entity entityContext = null, bool forceContextual = false) { if (commandFrame != null) { DisableCommandUI(); } @@ -2018,7 +2045,7 @@ namespace Barotrauma new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center), (spriteBatch, _) => { - if (!(entityContext is Character character) || character?.Info == null) { return; } + if (entityContext is not Character character || character?.Info == null) { return; } var node = startNode; character.Info.DrawJobIcon(spriteBatch, new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); @@ -2082,7 +2109,7 @@ namespace Barotrauma returnNodeMargin = returnNodeSize.X * 0.5f; nodeDistance = (int)(150 * GUI.Scale); - shorcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); + shorcutCenterNodeOffset = new Point(0, (int)(1.35f * nodeDistance)); } private List GetAvailableCategories() @@ -2115,7 +2142,7 @@ namespace Barotrauma if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); // Don't draw connectors for assignment nodes - if (!(optionNodes.FirstOrDefault()?.Button.UserData is Character)) + if (optionNodes.FirstOrDefault()?.Button.UserData is not Character) { // Regular option nodes if (targetFrame == null || !targetFrame.Visible) @@ -2194,7 +2221,7 @@ namespace Barotrauma private bool NavigateForward(GUIButton node, object userData) { if (commandFrame == null) { return false; } - if (!(optionNodes.Find(n => n.Button == node) is OptionNode optionNode) || !optionNodes.Remove(optionNode)) + if (optionNodes.Find(n => n.Button == node) is not OptionNode optionNode || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; @@ -2352,7 +2379,7 @@ namespace Barotrauma matchingItems = nodeOrder.GetMatchingItems(submarine, true, interactableFor: characterContext ?? Character.Controlled); } //more than one target item -> create a minimap-like selection with a pic of the sub - if (itemContext == null && !(nodeOrder.TargetEntity is Item) && matchingItems != null && matchingItems.Count > 1) + if (itemContext == null && nodeOrder.TargetEntity is not Item && matchingItems != null && matchingItems.Count > 1) { CreateMinimapNodes(nodeOrder, submarine, matchingItems); } @@ -2417,7 +2444,7 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); var icon = OrderCategoryIcon.OrderCategoryIcons.FirstOrDefault(ic => ic.Category == category); - if (!(icon is null)) + if (icon is not null) { var tooltip = TextManager.Get($"ordercategorytitle.{category}"); var categoryDescription = TextManager.Get($"ordercategorydescription.{category}"); @@ -2484,7 +2511,7 @@ namespace Barotrauma // TODO: Doesn't work for player issued reports, because they don't have a target. bool useSpecificRepairOrder = false; if (CanFitMoreNodes() && ShouldDelegateOrder("repairelectrical") && - ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.RequiredSkills.Any(s => s.Identifier == "electrical"))) { if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairelectrical"])) { @@ -2493,7 +2520,7 @@ namespace Barotrauma useSpecificRepairOrder = true; } if (CanFitMoreNodes() && ShouldDelegateOrder("repairmechanical") && - ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.RequiredSkills.Any(s => s.Identifier == "mechanical"))) { if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairmechanical"])) { @@ -2565,7 +2592,7 @@ namespace Barotrauma static bool ShouldDelegateOrder(string orderIdentifier) => ShouldDelegateOrderId(orderIdentifier.ToIdentifier()); static bool ShouldDelegateOrderId(Identifier orderIdentifier) { - return !(Character.Controlled is Character c) || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); + return Character.Controlled is not Character c || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); } bool IsNonDuplicateOrder(Order order) => IsNonDuplicateOrderPrefab(order.Prefab, order.Option); bool IsNonDuplicateOrderPrefab(OrderPrefab orderPrefab, Identifier option = default) @@ -2642,6 +2669,7 @@ namespace Barotrauma new Order(p, itemContext, targetComponent)); } } + // If targeting a periscope connected to a turret, show the 'operateweapons' order var operateWeaponsPrefab = OrderPrefab.Prefabs["operateweapons"]; if (contextualOrders.None(o => o.Identifier == "operateweapons") && itemContext.Components.Any(c => c is Controller)) @@ -2656,11 +2684,11 @@ namespace Barotrauma // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order if (contextualOrders.None(order => order.Identifier == "repairsystems") && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) { - if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) + if (itemContext.Repairables.Any(r => r != null && r.RequiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null)); } - else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) + else if (itemContext.Repairables.Any(r => r != null && r.RequiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null)); } @@ -2694,14 +2722,14 @@ namespace Barotrauma } void AddIgnoreOrder(IIgnorable target) { - var orderIdentifier = "ignorethis"; + var orderIdentifier = Tags.IgnoreThis; if (!target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); } else { - orderIdentifier = "unignorethis"; + orderIdentifier = Tags.UnignoreThis; if (target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); @@ -2753,20 +2781,6 @@ namespace Barotrauma } } - // TODO: there's duplicate logic here and above -> would be better to refactor so that the conditions are only defined in one place - public static bool DoesItemHaveContextualOrders(Item item) - { - if (OrderPrefab.Prefabs.Any(o => o.TargetItemsMatchItem(item))) { return true; } - if (OrderPrefab.Prefabs.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } - if (AIObjectiveCleanupItems.IsValidTarget(item, Character.Controlled, checkInventory: false)) { return true; } - if (AIObjectiveCleanupItems.IsValidContainer(item, Character.Controlled)) { return true; } - if (OrderPrefab.Prefabs.TryGet("loaditems", out OrderPrefab loadItemsPrefab) && AIObjectiveLoadItems.IsValidTarget(item, Character.Controlled, targetContainerTags: loadItemsPrefab.GetTargetItems())) { return true; } - if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { return true; } - return OrderPrefab.Prefabs.TryGet("operateweapons", out OrderPrefab operateWeaponsPrefab) && item.Components.Any(c => c is Controller) && - (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) || - item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item))); - } - /// Use a negative value (e.g. -1) if there should be no hotkey associated with the node private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true) { @@ -3719,16 +3733,16 @@ namespace Barotrauma switch (order.TargetType) { case Order.OrderTargetType.Entity: - if (!(order.TargetEntity is IIgnorable ignorableEntity)) { break; } - ignorableEntity.OrderedToBeIgnored = order.Identifier == "ignorethis"; + if (order.TargetEntity is not IIgnorable ignorableEntity) { break; } + ignorableEntity.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; break; case Order.OrderTargetType.Position: throw new NotImplementedException(); case Order.OrderTargetType.WallSection: if (!order.WallSectionIndex.HasValue) { break; } - if (!(order.TargetEntity is Structure s)) { break; } - if (!(s.GetSection(order.WallSectionIndex.Value) is IIgnorable ignorableWall)) { break; } - ignorableWall.OrderedToBeIgnored = order.Identifier == "ignorethis"; + if (order.TargetEntity is not Structure s) { break; } + if (s.GetSection(order.WallSectionIndex.Value) is not IIgnorable ignorableWall) { break; } + ignorableWall.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis; break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index feb6eb7d9..6571cebd7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -3,16 +3,19 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Barotrauma { - abstract partial class CampaignMode : GameMode + internal abstract partial class CampaignMode : GameMode { - protected bool crewDead; + public bool CrewDead + { + get; + protected set; + } protected Color overlayColor; protected Sprite overlaySprite; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index ce6972067..f0b7616cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -67,10 +67,11 @@ namespace Barotrauma var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), layout.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, isHorizontal: true) { + Stretch = true, RelativeSpacing = 0.02f }; - var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), layout.RectTransform, Anchor.BottomLeft), style: "InnerFrame") + var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), layout.RectTransform, Anchor.BottomLeft), style: "GUIFrameListBox") { CanBeFocused = false }; @@ -95,6 +96,7 @@ namespace Barotrauma loadCampaignButton.Selected = false; newCampaignContainer.Visible = true; loadCampaignContainer.Visible = false; + GameMain.NetLobbyScreen?.RefreshStartButtonVisibility(); return true; }; loadCampaignButton.OnClicked = (btn, obj) => @@ -103,6 +105,7 @@ namespace Barotrauma loadCampaignButton.Selected = true; newCampaignContainer.Visible = false; loadCampaignContainer.Visible = true; + GameMain.NetLobbyScreen?.RefreshStartButtonVisibility(); return true; }; loadCampaignContainer.Visible = false; @@ -297,7 +300,7 @@ namespace Barotrauma Level prevLevel = Level.Loaded; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); - crewDead = false; + CrewDead = false; var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; if (continueButton != null) @@ -480,7 +483,6 @@ namespace Barotrauma GameMain.CampaignEndScreen.OnFinished = () => { GameMain.NetLobbyScreen.Select(); - if (GameMain.NetLobbyScreen.ContinueCampaignButton != null) { GameMain.NetLobbyScreen.ContinueCampaignButton.Enabled = false; } if (GameMain.NetLobbyScreen.QuitCampaignButton != null) { GameMain.NetLobbyScreen.QuitCampaignButton.Enabled = false; } }; } @@ -934,7 +936,7 @@ namespace Barotrauma { int renamedIdentifier = msg.ReadInt32(); string newName = msg.ReadString(); - CharacterInfo renamedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); if (renamedCharacter != null) { CrewManager.RenameCharacter(renamedCharacter, newName); } } @@ -942,7 +944,7 @@ namespace Barotrauma if (fireCharacter) { int firedIdentifier = msg.ReadInt32(); - CharacterInfo firedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); + CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); // this one might and is allowed to be null since the character is already fired on the original sender's game if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } } @@ -952,7 +954,7 @@ namespace Barotrauma !NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) { CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); - if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); } + if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters, takeMoney: false); } CampaignUI.CrewManagement.SetPendingHires(pendingHires, map.CurrentLocation); if (renameCrewMember || fireCharacter) { CampaignUI.CrewManagement.UpdateCrew(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 803e3c275..f16ba4ad8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -243,7 +244,7 @@ namespace Barotrauma savedOnStart = true; } - crewDead = false; + CrewDead = false; endTimer = 5.0f; CrewManager.InitSinglePlayerRound(); LoadPets(); @@ -373,7 +374,7 @@ namespace Barotrauma SoundPlayer.OverrideMusicType = (success ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; GUI.SetSavingIndicatorState(success); - crewDead = false; + CrewDead = false; if (success) { @@ -582,9 +583,12 @@ namespace Barotrauma HintManager.OnAvailableTransition(transitionType); } - if (!crewDead) + if (!CrewDead) { - if (!CrewManager.GetCharacters().Any(c => !c.IsDead)) { crewDead = true; } + if (CrewManager.GetCharacters().None(c => !c.IsDead && !CrewManager.IsFired(c))) + { + CrewDead = true; + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 725d24de9..c945b6103 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -42,6 +42,15 @@ namespace Barotrauma foreach (Submarine submarine in Submarine.Loaded) { submarine.NeutralizeBallast(); + //normally the body would be made static during level generation, + //but in the test mode we load the outpost/wreck/beacon as if it was a normal sub and need to do this manually + if (submarine.Info.Type == SubmarineType.Outpost || + submarine.Info.Type == SubmarineType.OutpostModule || + submarine.Info.Type == SubmarineType.Wreck || + submarine.Info.Type == SubmarineType.BeaconStation) + { + submarine.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static; + } } if (SpawnOutpost) @@ -51,14 +60,14 @@ namespace Barotrauma if (TriggeredEvent != null) { - scriptedEvent = new List { TriggeredEvent.CreateInstance() }; + scriptedEvent = new List { TriggeredEvent.CreateInstance(GameMain.GameSession.EventManager.RandomSeed) }; GameMain.GameSession.EventManager.PinnedEvent = scriptedEvent.Last(); createEventButton = new GUIButton(new RectTransform(new Point(128, 64), GUI.Canvas, Anchor.TopCenter) { ScreenSpaceOffset = new Point(0, 32) }, TextManager.Get("create")) { OnClicked = delegate { - scriptedEvent.Add(TriggeredEvent.CreateInstance()); + scriptedEvent.Add(TriggeredEvent.CreateInstance(GameMain.GameSession.EventManager.RandomSeed)); GameMain.GameSession.EventManager.PinnedEvent = scriptedEvent.Last(); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 5284339d0..34cde04bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -190,9 +190,9 @@ namespace Barotrauma.Tutorials var door = item.GetComponent(); if (door != null) { - if (door.requiredItems.Values.None(ris => ris.None(ri => ri.Identifiers.None(i => i == "locked")))) + if (door.RequiredItems.Values.None(ris => ris.None(ri => ri.Identifiers.None(i => i == "locked")))) { - door.requiredItems.Clear(); + door.RequiredItems.Clear(); } } } @@ -264,7 +264,7 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Failure; } - if (eventPrefab.CreateInstance() is Event eventInstance) + if (eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed) is Event eventInstance) { GameMain.GameSession.EventManager.QueuedEvents.Enqueue(eventInstance); while (!eventInstance.IsFinished) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 7ce88ca26..ff5940862 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -110,7 +111,9 @@ namespace Barotrauma }; respawnTickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, respawnButtonContainer.RectTransform, Anchor.Center), TextManager.Get("respawnquestionpromptrespawn")) { - ToolTip = TextManager.Get("respawnquestionprompt"), + ToolTip = TextManager.GetWithVariable( + "respawnquestionprompt", "[percentage]", + (Math.Round(Networking.RespawnManager.SkillLossPercentageOnImmediateRespawn).ToString())), OnSelected = (tickbox) => { GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: !tickbox.Selected); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 8d222c5a2..d859a5af5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -185,6 +185,19 @@ namespace Barotrauma } } + public static void OnItemMarkedForRelocation() + { + DisplayHint($"onitemmarkedforrelocation".ToIdentifier()); + } + + public static void OnItemMarkedForDeconstruction(Character character) + { + if (character == Character.Controlled) + { + DisplayHint($"onitemmarkedfordeconstruction".ToIdentifier()); + } + } + private static void CheckIsInteracting() { if (!CanDisplayHints()) { return; } @@ -582,6 +595,24 @@ namespace Barotrauma { DisplayHint("onballastflorainfected".ToIdentifier()); } + if (order.Identifier == "deconstructitems" && + Item.DeconstructItems.None()) + { + DisplayHint("ondeconstructorder".ToIdentifier()); + } + } + + public static void OnSetOrder(Character character, Order order) + { + if (!CanDisplayHints()) { return; } + if (character == null || order == null) { return; } + + if (order.OrderGiver == Character.Controlled && + order.Identifier == "deconstructitems" && + Item.DeconstructItems.None()) + { + DisplayHint("ondeconstructorder".ToIdentifier()); + } } private static void CheckIfDivingGearOutOfOxygen() @@ -719,7 +750,7 @@ namespace Barotrauma HintsIgnoredThisRound.Add(hintIdentifier); - ActiveHintMessageBox = new GUIMessageBox(hintIdentifier, text, icon); + ActiveHintMessageBox = new GUIMessageBox(hintIdentifier, TextManager.ParseInputTypes(text), icon); if (iconColor.HasValue) { ActiveHintMessageBox.IconColor = iconColor.Value; } OnUpdate = onUpdate; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 068477a41..8ab663787 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -822,7 +822,7 @@ namespace Barotrauma factionTextContent.Recalculate(); new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), - onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue)); + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue, reputation.MinReputation, reputation.MaxReputation)); var reputationText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), string.Empty, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); @@ -871,7 +871,7 @@ namespace Barotrauma return factionFrame; } - public static void DrawReputationBar(SpriteBatch sb, Rectangle rect, float normalizedReputation) + public static void DrawReputationBar(SpriteBatch sb, Rectangle rect, float normalizedReputation, float minReputation, float maxReputation) { int segmentWidth = rect.Width / 5; rect.Width = segmentWidth * 5; @@ -885,9 +885,10 @@ namespace Barotrauma GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.TextColorNormal, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); - GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), "-100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); - Vector2 textSize = GUIStyle.SmallFont.MeasureString("100"); - GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), "100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); + GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), ((int)minReputation).ToString(), GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); + string maxRepText = ((int)maxReputation).ToString(); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(maxRepText); + GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), maxRepText, GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs index 073893bb2..da7d33546 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs @@ -1,7 +1,6 @@ #nullable enable -using System; -using System.Linq; using Barotrauma.Networking; +using System.Linq; namespace Barotrauma { @@ -13,7 +12,7 @@ namespace Barotrauma if (character != null) { - Speak(character); + character.Speak(text, ChatMessageType.Default); return; } @@ -21,23 +20,10 @@ namespace Barotrauma { if (npc.CampaignInteractionType == CampaignMode.InteractionType.Upgrade) { - Speak(npc); + npc.Speak(text, ChatMessageType.Default); break; } } - - void Speak(Character npc) - { - ChatMessage message = ChatMessage.Create(npc.Name, text, ChatMessageType.Default, npc); - if (!isSinglePlayer) - { - GameMain.Client?.AddChatMessage(message); - } - else - { - GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(message); - } - } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 705b59f8f..2b9ec4112 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -771,7 +771,7 @@ namespace Barotrauma { if (Screen.Selected == GameMain.GameScreen) { - if (item.NonInteractable || item.NonPlayerTeamInteractable) + if (!item.IsInteractable(Character.Controlled)) { return QuickUseAction.None; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index f7062ac72..5ea8a5265 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -75,6 +75,7 @@ namespace Barotrauma.Items.Components private void UpdateConvexHulls() { if (item.Removed) { return; } + if (doorSprite == null) { return; } doorRect = new Rectangle( item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale), @@ -83,7 +84,7 @@ namespace Barotrauma.Items.Components (int)(doorSprite.size.Y * item.Scale)); Rectangle rect = doorRect; - if (IsHorizontal) + if (IsConvexHullHorizontal) { rect.Width = (int)(rect.Width * (1.0f - openState)); } @@ -94,7 +95,7 @@ namespace Barotrauma.Items.Components if (Window.Height > 0 && Window.Width > 0) { - if (IsHorizontal) + if (IsConvexHullHorizontal) { rect.Width = (int)(Window.X * item.Scale); rect.X -= (int)(doorRect.Width * openState); @@ -163,8 +164,8 @@ namespace Barotrauma.Items.Components var verts = GetConvexHullCorners(rect); Vector2 center = (verts[0] + verts[2]) / 2; convexHull.SetVertices( - verts, - IsHorizontal ? + verts, + IsConvexHullHorizontal ? new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } : new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) }); convexHull.MaxMergeLosVerticesDist = 35.0f; @@ -304,6 +305,7 @@ namespace Barotrauma.Items.Components } else { + bool stateChanged = open != isOpen; isOpen = open; if (!isNetworkMessage || open != PredictedState) { @@ -314,6 +316,11 @@ namespace Barotrauma.Items.Components } if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } } + if (stateChanged) + { + ActionType actionType = open ? ActionType.OnOpen : ActionType.OnClose; + item.ApplyStatusEffects(actionType, deltaTime: 1.0f); + } } void PlayInteractionSound() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index d0f607e20..b24344c84 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -64,7 +64,8 @@ namespace Barotrauma.Items.Components spriteBatch, new Vector2(attachPos.X, -attachPos.Y), item.SpriteColor * 0.5f, - 0.0f, item.Scale, SpriteEffects.None, 0.0f); + item.RotationRad, + item.Scale, SpriteEffects.None, 0.0f); GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUIStyle.Red, thickness: 3); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 099f54b34..352ba2c30 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -18,11 +18,31 @@ namespace Barotrauma.Items.Components protected float currentCrossHairScale, currentCrossHairPointerScale; private RoundSound chargeSound; + private SoundChannel chargeSoundChannel; + + [Serialize(defaultValue: "0.5, 1.5", IsPropertySaveable.No, description: "Pitch slides from X to Y over the charge time")] + public Vector2 ChargeSoundWindupPitchSlide + { + get => _chargeSoundWindupPitchSlide; + set + { + _chargeSoundWindupPitchSlide = new Vector2( + Math.Max(value.X, SoundChannel.MinFrequencyMultiplier), + Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier)); + } + } + private Vector2 _chargeSoundWindupPitchSlide; private readonly List particleEmitters = new List(); private readonly List particleEmitterCharges = new List(); + /// + /// The orientation of the item is briefly wrong after the character holding it flips and before the holding logic forces it to the correct position. + /// We disable the crosshair briefly during that time to prevent it from momentarily jumping to an incorrect position. + /// + private float crossHairPosDirtyTimer; + [Serialize(1.0f, IsPropertySaveable.No, description: "The scale of the crosshair sprite (if there is one).")] public float CrossHairScale { @@ -30,9 +50,9 @@ namespace Barotrauma.Items.Components private set; } - partial void InitProjSpecific(ContentXElement element) + partial void InitProjSpecific(ContentXElement rangedWeaponElement) { - foreach (var subElement in element.Elements()) + foreach (var subElement in rangedWeaponElement.Elements()) { string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) @@ -62,6 +82,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam) { + crossHairPosDirtyTimer -= deltaTime; currentCrossHairScale = currentCrossHairPointerScale = cam == null ? 1.0f : cam.Zoom; if (crosshairSprite != null) { @@ -92,6 +113,15 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } + public override void FlipX(bool relativeToSub) + { + crossHairPosDirtyTimer = 0.02f; + } + public override void FlipY(bool relativeToSub) + { + crossHairPosDirtyTimer = 0.02f; + } + partial void UpdateProjSpecific(float deltaTime) { float chargeRatio = currentChargeTime / MaxChargeTime; @@ -117,7 +147,7 @@ namespace Barotrauma.Items.Components } else if (chargeSoundChannel != null) { - chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(ChargeSoundWindupPitchSlide.X, ChargeSoundWindupPitchSlide.Y, chargeRatio); chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; @@ -143,15 +173,19 @@ namespace Barotrauma.Items.Components if (character == null || !character.IsKeyDown(InputType.Aim) || !character.CanAim) { return; } //camera focused on some other item/device, don't draw the crosshair - if (character.ViewTarget != null && (character.ViewTarget is Item viewTargetItem) && viewTargetItem.Prefab.FocusOnSelected) { return; } + if (character.ViewTarget is Item viewTargetItem && viewTargetItem.Prefab.FocusOnSelected) { return; } //don't draw the crosshair if the item is in some other type of equip slot than hands (e.g. assault rifle in the bag slot) if (!character.HeldItems.Contains(item)) { return; } GUI.HideCursor = (crosshairSprite != null || crosshairPointerSprite != null) && GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; - if (GUI.HideCursor) + + if (GUI.HideCursor && !character.AnimController.IsHoldingToRope) { - crosshairSprite?.Draw(spriteBatch, crosshairPos, ReloadTimer <= 0.0f ? Color.White : Color.White * 0.2f, 0, currentCrossHairScale); + if (crossHairPosDirtyTimer <= 0.0f) + { + crosshairSprite?.Draw(spriteBatch, crosshairPos, ReloadTimer <= 0.0f ? Color.White : Color.White * 0.2f, 0, currentCrossHairScale); + } crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, currentCrossHairPointerScale); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 21b3c9c76..dfa27902a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -205,7 +205,7 @@ namespace Barotrauma.Items.Components int buttonSize = GUIStyle.ItemFrameTopBarHeight; Point margin = new Point(buttonSize / 4, buttonSize / 6); - GUILayoutGroup buttonArea = new GUILayoutGroup(new RectTransform(new Point(GuiFrame.Rect.Width - margin.X * 2, buttonSize - margin.Y * 2), GuiFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, margin.Y) }, + GUILayoutGroup buttonArea = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, buttonSize - margin.Y * 2), content.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(0, margin.Y) }, isHorizontal: true, childAnchor: Anchor.TopRight) { AbsoluteSpacing = margin.X / 2 @@ -334,7 +334,7 @@ namespace Barotrauma.Items.Components } else { - return item?.Name; + return item?.Prefab.Name; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 9e4c1b164..8bbdc47c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -40,6 +40,7 @@ namespace Barotrauma.Items.Components partial void SetLightSourceState(bool enabled, float brightness) { if (Light == null) { return; } + if (item.HiddenInGame) { enabled = false; } Light.Enabled = enabled; lightColorMultiplier = brightness; if (enabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index ba8b62d21..0fbbf4c00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.08f }; - new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform) { MinSize = new Point(0, GUI.IntScale(25)) }, item.Name, font: GUIStyle.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform) { MinSize = new Point(0, GUI.IntScale(25)) }, item.Prefab.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleHorizontal = true @@ -341,7 +341,7 @@ namespace Barotrauma.Items.Components GUIFrame itemFrame = new GUIFrame(new RectTransform(new Vector2(0.1f, 1f), parent.RectTransform), style: null) { UserData = identifier, - ToolTip = GetTooltip(prefab) + ToolTip = prefab.CreateTooltipText() }; Sprite icon = prefab.InventoryIcon ?? prefab.Sprite; @@ -372,21 +372,6 @@ namespace Barotrauma.Items.Components textBlock.Text = TextManager.GetWithVariable("campaignstore.quantity", "[amount]", count.ToString()); } - - static RichString GetTooltip(ItemPrefab prefab) - { - LocalizedString toolTip = $"‖color:{Color.White.ToStringHex()}‖{prefab.Name}‖color:end‖"; - - LocalizedString description = prefab.Description; - if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } - - if (prefab.ContentPackage != GameMain.VanillaContent && prefab.ContentPackage != null) - { - toolTip += $"\n‖color:{Color.MediumPurple.ToStringHex()}‖{prefab.ContentPackage.Name}‖color:end‖"; - } - - return RichString.Rich(toolTip); - } } partial void OnItemLoadedProjSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 24fbcfb36..db034ffc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -134,11 +134,11 @@ namespace Barotrauma.Items.Components spriteIndex += (force / 100.0f) * AnimSpeed * deltaTime; if (spriteIndex < 0) { - spriteIndex = propellerSprite.FrameCount; + spriteIndex = propellerSprite.FrameCount - Math.Abs(spriteIndex) % propellerSprite.FrameCount; } - if (spriteIndex >= propellerSprite.FrameCount) + else { - spriteIndex = 0.0f; + spriteIndex = spriteIndex % propellerSprite.FrameCount; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 96f708e8a..0596825f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); // === LABEL === // - new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Name, font: GUIStyle.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Prefab.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleVertical = true @@ -326,7 +326,7 @@ namespace Barotrauma.Items.Components UserData = fi, HoverColor = Color.Gold * 0.2f, SelectedColor = Color.Gold * 0.5f, - ToolTip = fi.TargetItem.Description + ToolTip = RichString.Rich(fi.TargetItem.Description) }; var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), @@ -339,7 +339,7 @@ namespace Barotrauma.Items.Components itemIcon, scaleToFit: true) { Color = fi.TargetItem.InventoryIconColor, - ToolTip = fi.TargetItem.Description + ToolTip = RichString.Rich(fi.TargetItem.Description) }; } @@ -347,7 +347,7 @@ namespace Barotrauma.Items.Components { Padding = Vector4.Zero, AutoScaleVertical = true, - ToolTip = fi.TargetItem.Description + ToolTip = RichString.Rich(fi.TargetItem.Description) }; new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight), @@ -925,8 +925,6 @@ namespace Barotrauma.Items.Components nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W); if (nameBlock.TextScale < 0.7f) { - nameBlock.SetRichText(TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName) - .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName))); nameBlock.AutoScaleHorizontal = false; nameBlock.TextScale = 0.7f; nameBlock.Wrap = true; @@ -937,7 +935,7 @@ namespace Barotrauma.Items.Components if (!selectedItem.TargetItem.Description.IsNullOrEmpty()) { var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - selectedItem.TargetItem.Description, + RichString.Rich(selectedItem.TargetItem.Description), font: GUIStyle.SmallFont, wrap: true); description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 48b20a156..dc0eeb6dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -1090,7 +1090,12 @@ namespace Barotrauma.Items.Components float totalVolume = 0.0f; foreach (Hull linkedHull in hullData.LinkedHulls) { - waterVolume += linkedHull.WaterVolume; + //water detector ignores very small amounts of water, + //do it here too so the nav terminal doesn't display the water + if (WaterDetector.GetWaterPercentage(linkedHull) > 0.0f) + { + waterVolume += linkedHull.WaterVolume; + } totalVolume += linkedHull.Volume; } hullData.HullWaterAmount = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index 8c09ef322..67652e781 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components public override bool Select(Character character) { - if (GameMain.GameSession?.Campaign == null) + if (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 1e636ec15..4f3f08ffe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1446,7 +1446,7 @@ namespace Barotrauma.Items.Components //only relevant in the end levels or maybe custom subs with some kind of non-hulled parts Rectangle worldBorders = submarine.GetDockedBorders(); worldBorders.Location += submarine.WorldPosition.ToPoint(); - if (Submarine.RectContains(worldBorders, pingSource)) + if (Submarine.RectContains(worldBorders, pingSource) || submarine.Info.OutpostGenerationParams is { AlwaysShowStructuresOnSonar: true }) { CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive); continue; @@ -1502,7 +1502,9 @@ namespace Barotrauma.Items.Components foreach (Voronoi2.GraphEdge edge in cell.Edges) { if (!edge.IsSolid) { continue; } - float cellDot = Vector2.Dot(cell.Center - pingSource, (edge.Center + cell.Translation) - cell.Center); + + //the normal of the edge must be pointing towards the ping source to be visible + float cellDot = Vector2.Dot((edge.Center + cell.Translation) - pingSource, edge.GetNormal(cell)); if (cellDot > 0) { continue; } float facingDot = Vector2.Dot( @@ -1543,6 +1545,7 @@ namespace Barotrauma.Items.Components { if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; } if (!c.IsUnconscious && c.Params.HideInSonar) { continue; } + if (c.InDetectable) { continue; } if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (c.AnimController.SimplePhysicsEnabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 90d3f85ed..4d9aabff1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -316,28 +316,28 @@ namespace Barotrauma.Items.Components { case 0: leftText = TextManager.Get("DescentVelocity"); - centerText = $"({TextManager.Get("KilometersPerHour")})"; + centerText = TextManager.Get("KilometersPerHour"); rightTextGetter = () => { Vector2 vel = controlledSub == null ? Vector2.Zero : controlledSub.Velocity; var realWorldVel = ConvertUnits.ToDisplayUnits(vel.Y * Physics.DisplayToRealWorldRatio) * 3.6f; - return ((int)(-realWorldVel)).ToString(); + return (-realWorldVel).ToString("0.0"); }; break; case 1: leftText = TextManager.Get("Velocity"); - centerText = $"({TextManager.Get("KilometersPerHour")})"; + centerText = TextManager.Get("KilometersPerHour"); rightTextGetter = () => { Vector2 vel = controlledSub == null ? Vector2.Zero : controlledSub.Velocity; var realWorldVel = ConvertUnits.ToDisplayUnits(vel.X * Physics.DisplayToRealWorldRatio) * 3.6f; if (controlledSub != null && controlledSub.FlippedX) { realWorldVel *= -1; } - return ((int)realWorldVel).ToString(); + return realWorldVel.ToString("0.0"); }; break; case 2: leftText = TextManager.Get("Depth"); - centerText = $"({TextManager.Get("Meter")})"; + centerText = TextManager.Get("Meter"); rightTextGetter = () => { if (Level.Loaded is { IsEndBiome: true }) @@ -789,9 +789,9 @@ namespace Barotrauma.Items.Components } pressureWarningText.Visible = item.Submarine != null && Timing.TotalTime % 1.0f < 0.8f; - float depthEffectThreshold = 500.0f; if (Level.Loaded != null && pressureWarningText.Visible && - item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - depthEffectThreshold && item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - depthEffectThreshold) + item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - PressureWarningThreshold && + item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - PressureWarningThreshold) { pressureWarningText.Visible = true; pressureWarningText.Text = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index fd794ea11..0b551f6bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -148,11 +148,29 @@ namespace Barotrauma.Items.Components Vector2 particlePos = item.WorldPosition; float rotation = -item.body.Rotation; if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } + + //if the position is in a sub's local coordinates, convert to world coordinates + particlePos = ConvertToWorldCoordinates(particlePos); + //if the start location is in a sub's local coordinates, convert to world coordinates + startLocation = ConvertToWorldCoordinates(startLocation); + //same for end location + endLocation = ConvertToWorldCoordinates(endLocation); + Tuple tracerPoints = new Tuple(startLocation, endLocation); foreach (ParticleEmitter emitter in particleEmitters) { emitter.Emit(1.0f, particlePos, hullGuess: null, angle: rotation, particleRotation: rotation, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier, tracerPoints: tracerPoints); } + + static Vector2 ConvertToWorldCoordinates(Vector2 position) + { + Submarine containing = Submarine.FindContainingInLocalCoordinates(position); + if (containing != null) + { + position += containing.Position; + } + return position; + } } partial void InitProjSpecific(ContentXElement element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 59010730c..a05f47a0f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -63,10 +63,7 @@ namespace Barotrauma.Items.Components if (item.HiddenInGame) { return false; } if (!HasRequiredItems(character, false) || character.SelectedItem != item) { return false; } if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } - - float defaultMaxCondition = item.MaxCondition / item.MaxRepairConditionMultiplier; - - if (MathUtils.Percentage(item.Condition, defaultMaxCondition) < RepairThreshold) { return true; } + if (item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold) { return true; } if (CurrentFixer == character) { @@ -135,13 +132,13 @@ namespace Barotrauma.Items.Components new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont); skillTextContainer = paddedFrame; - for (int i = 0; i < requiredSkills.Count; i++) + for (int i = 0; i < RequiredSkills.Count; i++) { var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillTextContainer.RectTransform), - " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), + " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + RequiredSkills[i].Identifier), ((int) Math.Round(RequiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), font: GUIStyle.SmallFont) { - UserData = requiredSkills[i] + UserData = RequiredSkills[i] }; } @@ -262,7 +259,7 @@ namespace Barotrauma.Items.Components } } - float conditionPercentage = item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f; + float conditionPercentage = item.ConditionPercentageRelativeToDefaultMaxCondition; for (int i = 0; i < particleEmitters.Count; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 79adeb549..c6b5417c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -2,13 +2,16 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Xml.Linq; +using Barotrauma.Sounds; namespace Barotrauma.Items.Components { partial class Rope : ItemComponent, IDrawableComponent { private Sprite sprite, startSprite, endSprite; + + private RoundSound snapSound, reelSound; + private SoundChannel reelSoundChannel; [Serialize(5, IsPropertySaveable.No)] public int SpriteWidth @@ -54,6 +57,19 @@ namespace Barotrauma.Items.Components Math.Abs(target.DrawPosition.Y - sourcePos.Y)) * 1.5f; } } + + [Serialize("1.0, 1.0", IsPropertySaveable.No, description: "When reeling in, the pitch slides from X to Y, depending on the length of the rope.")] + public Vector2 ReelSoundPitchSlide + { + get => _reelSoundPitchSlide; + set + { + _reelSoundPitchSlide = new Vector2( + Math.Max(value.X, SoundChannel.MinFrequencyMultiplier), + Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier)); + } + } + private Vector2 _reelSoundPitchSlide; partial void InitProjSpecific(ContentXElement element) { @@ -70,9 +86,28 @@ namespace Barotrauma.Items.Components case "endsprite": endSprite = new Sprite(subElement); break; + case "snapsound": + snapSound = RoundSound.Load(subElement); + break; + case "reelsound": + reelSound = RoundSound.Load(subElement); + break; } } } + + partial void UpdateProjSpecific() + { + if (isReelingIn && !Snapped) + { + PlaySound(reelSound, source.WorldPosition); + } + else + { + reelSoundChannel?.FadeOutAndDispose(); + reelSoundChannel = null; + } + } public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { @@ -184,6 +219,32 @@ namespace Barotrauma.Items.Components overrideColor ?? SpriteColor, depth: depth, width: width); } } + + private void PlaySound(RoundSound sound, Vector2 position) + { + if (sound == null) { return; } + if (sound == reelSound) + { + if (reelSoundChannel is not { IsPlaying: true }) + { + reelSoundChannel = SoundPlayer.PlaySound(sound.Sound, position, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); + if (reelSoundChannel != null) + { + reelSoundChannel.Looping = true; + } + } + else + { + reelSoundChannel.Position = new Vector3(position, 0); + reelSoundChannel.Gain = MathHelper.Lerp(0, 1.0f, MathUtils.InverseLerp(MinPullDistance, MaxLength, MathUtils.Pow(currentRopeLength, 1.5f))); + reelSoundChannel.FrequencyMultiplier = MathHelper.Lerp(ReelSoundPitchSlide.X, ReelSoundPitchSlide.Y, MathUtils.InverseLerp(MinPullDistance, MaxLength, currentRopeLength)); + } + } + else + { + SoundPlayer.PlaySound(sound.Sound, position, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); + } + } public void ClientEventRead(IReadMessage msg, float sendingTime) { @@ -191,30 +252,38 @@ namespace Barotrauma.Items.Components if (!snapped) { - UInt16 targetId = msg.ReadUInt16(); - UInt16 sourceId = msg.ReadUInt16(); + ushort targetId = msg.ReadUInt16(); + ushort sourceId = msg.ReadUInt16(); byte limbIndex = msg.ReadByte(); - Item target = Entity.FindEntityByID(targetId) as Item; - if (target == null) { return; } + if (Entity.FindEntityByID(targetId) is not Item target) { return; } var source = Entity.FindEntityByID(sourceId); - if (source is Character sourceCharacter && limbIndex >= 0 && limbIndex < sourceCharacter.AnimController.Limbs.Length) + switch (source) { - Limb sourceLimb = sourceCharacter.AnimController.Limbs[limbIndex]; - Attach(sourceLimb, target); - } - else if (source is ISpatialEntity spatialEntity) - { - Attach(spatialEntity, target); + case Character sourceCharacter when limbIndex >= 0 && limbIndex < sourceCharacter.AnimController.Limbs.Length: + { + Limb sourceLimb = sourceCharacter.AnimController.Limbs[limbIndex]; + Attach(sourceLimb, target); + sourceCharacter.AnimController.DragWithRope(); + break; + } + case ISpatialEntity spatialEntity: + Attach(spatialEntity, target); + break; } } } protected override void RemoveComponentSpecific() { - sprite?.Remove(); sprite = null; - startSprite?.Remove(); startSprite = null; - endSprite?.Remove(); endSprite = null; + sprite?.Remove(); + sprite = null; + startSprite?.Remove(); + startSprite = null; + endSprite?.Remove(); + endSprite = null; + reelSoundChannel?.FadeOutAndDispose(); + reelSoundChannel = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs index 3f466be5c..6eb381503 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs @@ -129,6 +129,7 @@ namespace Barotrauma.Items.Components public void RemoveComponents(IReadOnlyCollection node) { + if (Locked) { return; } var ids = node.Select(static n => n.ID).ToImmutableArray(); if (GameMain.NetworkMember is null) @@ -145,6 +146,7 @@ namespace Barotrauma.Items.Components public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two) { + if (Locked) { return; } if (GameMain.NetworkMember is null) { Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab); @@ -158,6 +160,7 @@ namespace Barotrauma.Items.Components public void RemoveWires(IReadOnlyCollection wires) { + if (Locked) { return; } var ids = wires.Select(static w => w.ID).ToImmutableArray(); if (GameMain.NetworkMember is null) { @@ -175,6 +178,7 @@ namespace Barotrauma.Items.Components var ids = ImmutableArray.CreateBuilder(); var ios = ImmutableArray.CreateBuilder(); + var labelIds = ImmutableArray.CreateBuilder(); foreach (var moveable in moveables) { @@ -188,6 +192,9 @@ namespace Barotrauma.Items.Components case CircuitBoxInputOutputNode io: ios.Add(io.NodeType); break; + case CircuitBoxLabelNode label: + labelIds.Add(label.ID); + break; } } @@ -195,12 +202,13 @@ namespace Barotrauma.Items.Components { SelectComponentsInternal(ids, controlledId, overwrite); SelectInputOutputInternal(ios, controlledId, overwrite); + SelectLabelsInternal(labelIds, controlledId, overwrite); return; } - if ((!ids.Any() && !ios.Any()) && !overwrite) { return; } + if (!ids.Any() && !ios.Any() && !labelIds.Any() && !overwrite) { return; } - CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), overwrite, controlledId)); + CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), overwrite, controlledId)); } public void SelectWires(IReadOnlyCollection wires, bool overwrite) @@ -222,8 +230,10 @@ namespace Barotrauma.Items.Components public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection moveables) { + if (Locked) { return; } var ids = ImmutableArray.CreateBuilder(); var ios = ImmutableArray.CreateBuilder(); + var labelIds = ImmutableArray.CreateBuilder(); foreach (CircuitBoxNode move in moveables) { @@ -235,23 +245,27 @@ namespace Barotrauma.Items.Components case CircuitBoxInputOutputNode io: ios.Add(io.NodeType); break; + case CircuitBoxLabelNode label: + labelIds.Add(label.ID); + break; } } if (GameMain.NetworkMember is null) { - MoveNodesInternal(ids, ios, moveAmount); + MoveNodesInternal(ids, ios, labelIds, moveAmount); return; } - if (!ids.Any() && !ios.Any()) { return; } + if (!ids.Any() && !ios.Any() && !labelIds.Any()) { return; } - CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), moveAmount)); + CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), moveAmount)); } public void AddComponent(ItemPrefab prefab, Vector2 pos) { + if (Locked) { return; } if (GameMain.NetworkMember is null) { ItemPrefab resource; @@ -276,6 +290,72 @@ namespace Barotrauma.Items.Components CreateClientEvent(new CircuitBoxAddComponentEvent(prefab.UintIdentifier, pos)); } + public void RenameLabel(CircuitBoxLabelNode label, Color color, NetLimitedString header, NetLimitedString body) + { + if (Locked) { return; } + if (GameMain.NetworkMember is null) + { + label.EditText(header, body); + label.Color = color; + return; + } + + CreateClientEvent(new CircuitBoxRenameLabelEvent(label.ID, color, header, body)); + } + + public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount) + { + if (Locked) { return; } + var resize = node.ResizeBy(dir, amount); + if (GameMain.NetworkMember is null) + { + node.ApplyResize(resize.Size, resize.Pos); + return; + } + + // TODO this needs to be refactored at some point, probably not now + // the problem here is that the circuit box supports resizing all nodes + // but we limit the resizing to only labels on the client + // and on the server we only have a network message that targets labels + // so if we ever want the ability to resize other nodes (could be useful) the network message + // needs to know what type of ID it's targeting + if (node is not ICircuitBoxIdentifiable identifiable) + { + DebugConsole.ThrowError("Tried to resize a node that doesn't have an ID."); + return; + } + + CreateClientEvent(new CircuitBoxResizeLabelEvent(identifiable.ID, resize.Pos, resize.Size)); + } + + public void AddLabel(Vector2 pos) + { + if (Locked) { return; } + if (GameMain.NetworkMember is null) + { + AddLabelInternal(ICircuitBoxIdentifiable.FindFreeID(Labels), GUIStyle.Blue, pos, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty); + return; + } + + CreateClientEvent(new CircuitBoxAddLabelEvent(pos, GUIStyle.Blue, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty)); + } + + public void RemoveLabel(IReadOnlyCollection labels) + { + if (Locked) { return; } + if (!labels.Any()) { return; } + + var ids = labels.Select(static n => n.ID).ToImmutableArray(); + + if (GameMain.NetworkMember is null) + { + RemoveLabelInternal(ids); + return; + } + + CreateClientEvent(new CircuitBoxRemoveLabelEvent(ids)); + } + public partial void OnViewUpdateProjSpecific() { UI?.MouseSnapshotHandler.UpdateConnections(); @@ -396,7 +476,7 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.MoveComponent: { var data = INetSerializableStruct.Read(msg); - MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + MoveNodesInternal(data.TargetIDs, data.IOs, data.LabelIDs, data.MoveAmount); break; } case CircuitBoxOpcode.UpdateSelection: @@ -406,8 +486,9 @@ namespace Barotrauma.Items.Components var nodeDict = data.ComponentIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); var wireDict = data.WireIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); var ioDict = data.InputOutputs.ToImmutableDictionary(static s => s.Type, static s => s.SelectedBy); + var labelDict = data.LabelIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy); - UpdateSelections(nodeDict, wireDict, ioDict); + UpdateSelections(nodeDict, wireDict, ioDict, labelDict); break; } case CircuitBoxOpcode.AddWire: @@ -426,11 +507,18 @@ namespace Barotrauma.Items.Components { Components.Clear(); Wires.Clear(); + Labels.Clear(); var data = INetSerializableStruct.Read(msg); foreach (var compData in data.Components) { AddComponentFromData(compData); } foreach (var wireData in data.Wires) { AddWireFromData(wireData); } + foreach (var labelData in data.Labels) + { + AddLabelInternal(labelData.ID, labelData.Color, labelData.Position, labelData.Header, labelData.Body); + ResizeLabelInternal(labelData.ID, labelData.Position, labelData.Size); + } + foreach (var node in InputOutputNodes) { node.Position = node.NodeType switch @@ -443,6 +531,31 @@ namespace Barotrauma.Items.Components wasInitializedByServer = true; break; } + case CircuitBoxOpcode.RenameLabel: + { + var data = INetSerializableStruct.Read(msg); + RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody); + break; + } + case CircuitBoxOpcode.AddLabel: + { + var data = INetSerializableStruct.Read(msg); + AddLabelInternal(data.ID, data.Color, data.Position, data.Header, data.Body); + ResizeLabelInternal(data.ID, data.Position, data.Size); + break; + } + case CircuitBoxOpcode.RemoveLabel: + { + var data = INetSerializableStruct.Read(msg); + RemoveLabelInternal(data.TargetIDs); + break; + } + case CircuitBoxOpcode.ResizeLabel: + { + var data = INetSerializableStruct.Read(msg); + ResizeLabelInternal(data.ID, data.Position, data.Size); + break; + } default: throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs index 88dde557b..a6ca45e36 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs @@ -12,11 +12,13 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null) { - if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } - - Vector2 pos = item.WorldPosition + TransformedDetectOffset; - pos.Y = -pos.Y; - GUI.DrawRectangle(spriteBatch, pos - new Vector2(rangeX, rangeY), new Vector2(rangeX, rangeY) * 2.0f, Color.Cyan * 0.5f, isFilled: false, thickness: 2); + if ((editing && MapEntity.SelectedList.Contains(item)) || + (ConnectionPanel.ShouldDebugDrawWiring && Character.Controlled?.SelectedItem == item)) + { + Vector2 pos = item.WorldPosition + TransformedDetectOffset; + pos.Y = -pos.Y; + GUI.DrawRectangle(spriteBatch, pos - new Vector2(rangeX, rangeY), new Vector2(rangeX, rangeY) * 2.0f, Color.Cyan * 0.5f, isFilled: false, thickness: 2); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index a2f10cce8..5021d59a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -137,6 +137,7 @@ namespace Barotrauma.Items.Components { if (c == equipper || !c.Enabled || c.Removed) { continue; } if (!ShowDeadCharacters && c.IsDead) { continue; } + if (c.InDetectable) { continue; } float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist < Range * Range) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index c43909cc7..febcfc55b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -118,6 +118,19 @@ namespace Barotrauma.Items.Components get; private set; } + + [Serialize(defaultValue: "0.5, 1.5", IsPropertySaveable.No, description: "Pitch slides from X to Y over the charge time")] + public Vector2 ChargeSoundWindupPitchSlide + { + get => _chargeSoundWindupPitchSlide; + set + { + _chargeSoundWindupPitchSlide = new Vector2( + Math.Max(value.X, SoundChannel.MinFrequencyMultiplier), + Math.Min(value.Y, SoundChannel.MaxFrequencyMultiplier)); + } + } + private Vector2 _chargeSoundWindupPitchSlide; partial void InitProjSpecific(ContentXElement element) { @@ -220,9 +233,9 @@ namespace Barotrauma.Items.Components { if (moveSound != null) { - moveSoundChannel.FadeOutAndDispose(); + moveSoundChannel?.FadeOutAndDispose(); moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling, freqMult: moveSound.GetRandomFrequencyMultiplier()); - if (moveSoundChannel != null) moveSoundChannel.Looping = true; + if (moveSoundChannel != null) { moveSoundChannel.Looping = true;} } } } @@ -268,7 +281,7 @@ namespace Barotrauma.Items.Components } else if (chargeSoundChannel != null) { - chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(ChargeSoundWindupPitchSlide.X, ChargeSoundWindupPitchSlide.Y, chargeRatio); chargeSoundChannel.Position = new Vector3(item.WorldPosition, 0.0f); } break; @@ -482,12 +495,12 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUIStyle.Green; + widget.Color = GUIStyle.Green; prevAngle = minRotation; }; widget.Deselected += () => { - widget.color = Color.Yellow; + widget.Color = Color.Yellow; item.CreateEditingHUD(); RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) @@ -513,7 +526,7 @@ namespace Barotrauma.Items.Components }; widget.PreDraw += (sprtBtch, deltaTime) => { - widget.tooltip = "Min: " + (int)MathHelper.ToDegrees(minRotation); + widget.Tooltip = "Min: " + (int)MathHelper.ToDegrees(minRotation); widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; }; }); @@ -526,12 +539,12 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUIStyle.Green; + widget.Color = GUIStyle.Green; prevAngle = maxRotation; }; widget.Deselected += () => { - widget.color = Color.Yellow; + widget.Color = Color.Yellow; item.CreateEditingHUD(); RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) @@ -557,7 +570,7 @@ namespace Barotrauma.Items.Components }; widget.PreDraw += (sprtBtch, deltaTime) => { - widget.tooltip = "Max: " + (int)MathHelper.ToDegrees(maxRotation); + widget.Tooltip = "Max: " + (int)MathHelper.ToDegrees(maxRotation); widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(maxRotation), (float)Math.Sin(maxRotation)) * coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; widget.Update(deltaTime); }; @@ -584,20 +597,20 @@ namespace Barotrauma.Items.Components Vector2 offset = new Vector2(size / 2 + 5, -10); if (!widgets.TryGetValue(id, out Widget widget)) { - widget = new Widget(id, size, Widget.Shape.Rectangle) + widget = new Widget(id, size, WidgetShape.Rectangle) { - color = Color.Yellow, - tooltipOffset = offset, - inputAreaMargin = 20, + Color = Color.Yellow, + TooltipOffset = offset, + InputAreaMargin = 20, RequireMouseOn = false }; widgets.Add(id, widget); initMethod?.Invoke(widget); } - widget.size = size; - widget.tooltipOffset = offset; - widget.thickness = thickness; + widget.Size = size; + widget.TooltipOffset = offset; + widget.Thickness = thickness; return widget; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 1846a30b6..176d5c98b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -168,11 +168,14 @@ namespace Barotrauma //TODO: define this in xml slotSpriteSmall = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(10, 6, 119, 120), null, 0); // Adjustment to match the old size of 75,71 - SlotSpriteSmall.size = new Vector2(SlotSpriteSmall.SourceRect.Width * 0.575f, SlotSpriteSmall.SourceRect.Height * 0.575f); + SlotSpriteSmall.size = new Vector2(SlotSpriteSmall.SourceRect.Width * SlotSpriteSmallScale, SlotSpriteSmall.SourceRect.Height * SlotSpriteSmallScale); } return slotSpriteSmall; } } + + public const float SlotSpriteSmallScale = 0.575f; + public static Sprite DraggableIndicator; public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator; @@ -211,6 +214,7 @@ namespace Barotrauma public RichString Tooltip { get; private set; } public int tooltipDisplayedCondition; + public bool tooltipShowedContextualOptions; public bool ForceTooltipRefresh; @@ -230,6 +234,7 @@ namespace Barotrauma { if (ForceTooltipRefresh) { return true; } if (Item == null) { return false; } + if (PlayerInput.KeyDown(InputType.ContextualCommand) != tooltipShowedContextualOptions) { return true; } return (int)Item.ConditionPercentage != tooltipDisplayedCondition; } @@ -244,6 +249,7 @@ namespace Barotrauma } Tooltip = GetTooltip(Item, itemsInSlot, Character.Controlled); tooltipDisplayedCondition = (int)Item.ConditionPercentage; + tooltipShowedContextualOptions = PlayerInput.KeyDown(InputType.ContextualCommand); } private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, Character character) @@ -323,19 +329,19 @@ namespace Barotrauma .TrimStart(); } - if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) + if (itemsInSlot.All(it => !it.IsInteractable(Character.Controlled))) { toolTip += " " + TextManager.Get("connectionlocked"); } if (!item.IsFullCondition && !item.Prefab.HideConditionInTooltip) { - string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull)); + string conditionColorStr = XMLExtensions.ToStringHex(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull)); toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; } if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null) { - colorStr = XMLExtensions.ColorToString(Color.MediumPurple); + colorStr = XMLExtensions.ToStringHex(Color.MediumPurple); toolTip += $"\n‖color:{colorStr}‖{item.Prefab.ContentPackage.Name}‖color:end‖"; } } @@ -350,7 +356,17 @@ namespace Barotrauma } #if DEBUG toolTip += $" ({item.Prefab.Identifier})"; -#endif +#endif + if (PlayerInput.KeyDown(InputType.ContextualCommand)) + { + toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖"; + } + else + { + var colorStr = XMLExtensions.ToStringHex(Color.LightGray * 0.7f); + toolTip += $"\n‖color:{colorStr}‖{TextManager.Get("itemmsg.morreoptionsavailable")}‖color:end‖"; + } + return RichString.Rich(toolTip); } } @@ -613,10 +629,7 @@ namespace Barotrauma slot.State = GUIComponent.ComponentState.None; if (mouseOn && (DraggingItems.Any() || selectedSlot == null || selectedSlot.Slot == slot) && DraggingInventory == null) - // && - //(highlightedSubInventories.Count == 0 || highlightedSubInventories.Contains(this) || highlightedSubInventorySlot?.Slot == slot || highlightedSubInventory.Owner == item)) - { - + { slot.State = GUIComponent.ComponentState.Hover; if (selectedSlot == null || (!selectedSlot.IsSubSlot && isSubSlot)) @@ -631,27 +644,47 @@ namespace Barotrauma if (!DraggingItems.Any()) { - var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => !it.NonInteractable && !it.NonPlayerTeamInteractable) : slots[slotIndex].Items; - if (PlayerInput.PrimaryMouseButtonDown() && interactableItems.Any()) - { - if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) + var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => it.IsInteractable(Character.Controlled)) : slots[slotIndex].Items; + if (interactableItems.Any()) + { + if (availableContextualOrder.target != null) { - DraggingItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2)); + if (PlayerInput.PrimaryMouseButtonClicked()) + { + GameMain.GameSession.CrewManager.SetCharacterOrder(character: null, + new Order(OrderPrefab.Prefabs[availableContextualOrder.orderIdentifier], availableContextualOrder.target, targetItem: null, orderGiver: Character.Controlled)); + } + availableContextualOrder = default; } - else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) + else if (PlayerInput.KeyDown(InputType.Command) && + PlayerInput.KeyDown(InputType.ContextualCommand) && + GameMain.GameSession?.CrewManager != null) { - DraggingItems.Add(interactableItems.First()); + GameMain.GameSession.CrewManager.OpenCommandUI(interactableItems.FirstOrDefault(), forceContextual: true); } - else + else if (PlayerInput.PrimaryMouseButtonDown()) { - DraggingItems.AddRange(interactableItems); + if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) + { + DraggingItems.AddRange(interactableItems.Skip(interactableItems.Count() / 2)); + } + else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) + { + DraggingItems.Add(interactableItems.First()); + } + else + { + DraggingItems.AddRange(interactableItems); + } + DraggingSlot = slot; } - DraggingSlot = slot; } } else if (PlayerInput.PrimaryMouseButtonReleased()) { - var interactableItems = Screen.Selected == GameMain.GameScreen ? slots[slotIndex].Items.Where(it => !it.NonInteractable && !it.NonPlayerTeamInteractable) : slots[slotIndex].Items; + var interactableItems = Screen.Selected == GameMain.GameScreen ? + slots[slotIndex].Items.Where(it => it.IsInteractable(Character.Controlled)) : + slots[slotIndex].Items; if (PlayerInput.DoubleClicked() && interactableItems.Any()) { doubleClickedItems.Clear(); @@ -1286,7 +1319,7 @@ namespace Barotrauma if (selectedInventory.GetItemAt(slotIndex)?.OwnInventory?.Container is { } container && container.Inventory.CanBePut(item)) { - if (!container.AllowDragAndDrop || !container.DrawInventory) + if (!container.AllowDragAndDrop || !container.AllowAccess) { allowCombine = false; } @@ -1562,10 +1595,22 @@ namespace Barotrauma { selectedSlot.RefreshTooltip(); } - DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); + + if (!slotIconTooltip.IsNullOrEmpty()) + { + DrawToolTip(spriteBatch, slotIconTooltip, slotRect); + } + else + { + DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); + } + slotIconTooltip = string.Empty; } } + private static (Item target, Identifier orderIdentifier) availableContextualOrder; + private static LocalizedString slotIconTooltip; + public static void DrawSlot(SpriteBatch spriteBatch, Inventory inventory, VisualSlot slot, Item item, int slotIndex, bool drawItem = true, InvSlotType type = InvSlotType.Any) { Rectangle rect = slot.Rect; @@ -1730,7 +1775,7 @@ namespace Barotrauma } Color spriteColor = sprite == item.Sprite ? item.GetSpriteColor() : item.GetInventoryIconColor(); - if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => it.NonInteractable || it.NonPlayerTeamInteractable))) { spriteColor *= 0.5f; } + if (inventory != null && (inventory.Locked || inventory.slots[slotIndex].Items.All(it => !it.IsInteractable(Character.Controlled)))) { spriteColor *= 0.5f; } if (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface && !item.AllowedSlots.Contains(InvSlotType.HealthInterface) && item.GetComponent() == null) { spriteColor = Color.Lerp(spriteColor, Color.TransparentBlack, 0.5f); @@ -1741,15 +1786,24 @@ namespace Barotrauma } sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale); - if (((item.SpawnedInCurrentOutpost && !item.AllowStealing) || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + if (item.OrderedToBeIgnored) { - var stealIcon = CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand]; - Vector2 iconSize = new Vector2(25 * GUI.Scale); - stealIcon.Draw( - spriteBatch, - new Vector2(rect.X + iconSize.X * 0.2f, rect.Bottom - iconSize.Y * 1.2f), - color: GUIStyle.Red, - scale: iconSize.X / stealIcon.size.X); + if (OrderPrefab.Prefabs.TryGet(Tags.IgnoreThis, out OrderPrefab ignoreOrder)) + { + DrawSideIcon(ignoreOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.ignored"), ignoreOrder.Color, out bool mouseOn); + if (mouseOn) { availableContextualOrder = (item, Tags.UnignoreThis); } + + } + } + else if (Item.DeconstructItems.Contains(item) && + OrderPrefab.Prefabs.TryGet(Tags.DeconstructThis, out OrderPrefab deconstructOrder)) + { + DrawSideIcon(deconstructOrder.SymbolSprite, Direction.Right, TextManager.Get("tooltip.markedfordeconstruction"), GUIStyle.Red, out bool mouseOn); + if (mouseOn) { availableContextualOrder = (item, Tags.DontDeconstructThis); } + } + else if (((item.SpawnedInCurrentOutpost && !item.AllowStealing) || (inventory != null && inventory.slots[slotIndex].Items.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + { + DrawSideIcon(CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand], Direction.Left, TextManager.Get("tooltip.stolenitem"), GUIStyle.Red, out _); } int maxStackSize = item.Prefab.GetMaxStackSize(inventory); if (inventory is ItemInventory itemInventory) @@ -1798,6 +1852,22 @@ namespace Barotrauma SpriteEffects.None, layerDepth: 0.0f); } + + void DrawSideIcon(Sprite icon, Direction side, LocalizedString tooltip, Color color, out bool mouseOn) + { + Vector2 iconSize = new Vector2(25 * GUI.Scale); + float margin = 0.2f; + Vector2 pos = new Vector2( + side == Direction.Left ? rect.X + iconSize.X * margin : rect.Right - iconSize.X * margin, + rect.Bottom - iconSize.Y * 1.2f); + mouseOn = Vector2.Distance(PlayerInput.MousePosition, pos) < iconSize.X / 2; + if (mouseOn) + { + slotIconTooltip = tooltip; + color = Color.Lerp(color, Color.White, 0.5f); + } + icon.Draw(spriteBatch, pos, color: color, scale: iconSize.X / icon.size.X); + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 6552ba373..5ff674fb4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -9,7 +9,10 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; +using System.Text; namespace Barotrauma { @@ -216,7 +219,7 @@ namespace Barotrauma } } - float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; + float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition; for (int i = 0; i < Prefab.BrokenSprites.Length;i++) { if (Prefab.BrokenSprites[i].FadeIn) { continue; } @@ -340,9 +343,28 @@ namespace Barotrauma bool renderTransparent = isWiringMode && GetComponent() == null; if (renderTransparent) { color *= 0.15f; } + if (Character.Controlled != null && Character.DebugDrawInteract) + { + color = Color.Red; + foreach (var ic in components) + { + var interactionType = GetComponentInteractionVisibility(Character.Controlled, ic); + if (interactionType == InteractionVisibility.MissingRequirement) + { + color = Color.Orange; + } + else if (interactionType == InteractionVisibility.Visible) + { + color = Color.LightGreen; + break; + } + } + } + BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; - float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; + + float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition; Vector2 drawOffset = GetCollapseEffectOffset(); drawOffset.Y = -drawOffset.Y; @@ -849,27 +871,6 @@ namespace Barotrauma itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) { - //create a tag picker for item containers to make it easier to pick relevant tags for PreferredContainers - var itemContainer = GetComponent(); - if (itemContainer != null) - { - var tagsField = itemEditor.Fields["Tags".ToIdentifier()].First().Parent; - - //find all the items that can be put inside the container and add their PreferredContainer identifiers/tags to the available tags - ImmutableHashSet availableTags = ItemPrefab.Prefabs - .Where(ip => itemContainer.CanBeContained(ip)) - .SelectMany(ip => ip.PreferredContainers.SelectMany(pc => pc.Primary.Union(pc.Secondary))) - //remove identifiers from the available container tags - //(otherwise the list will include many irrelevant options, - //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) - .Where(t => !ItemPrefab.Prefabs.ContainsKey(t)) - .ToImmutableHashSet(); - new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") - { - OnClicked = (bt, userData) => { CreateTagPicker(tagsField.GetChild(), availableTags); return true; } - }; - } - if (Linkable) { var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont); @@ -881,6 +882,33 @@ namespace Barotrauma linkText.TextColor = GUIStyle.Orange; itemsText.TextColor = GUIStyle.Orange; } + + //create a tag picker for item containers to make it easier to pick relevant tags for PreferredContainers + var itemContainer = GetComponent(); + if (itemContainer != null) + { + var tagBox = itemEditor.Fields["Tags".ToIdentifier()].First() as GUITextBox; + var tagsField = tagBox?.Parent; + + var containerTagLayout = new GUILayoutGroup(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), isHorizontal: true); + var containerTagButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1), containerTagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); + new GUIButton(new RectTransform(new Vector2(0.95f, 1), containerTagButtonLayout.RectTransform), text: TextManager.Get("containertaguibutton"), style: "GUIButtonSmall") + { + OnClicked = (_, _) => { CreateContainerTagPicker(tagBox); return true; }, + TextBlock = { AutoScaleHorizontal = true } + }; + var containerTagText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1), containerTagLayout.RectTransform), TextManager.Get("containertaguibuttondescription"), font: GUIStyle.SmallFont) + { + TextColor = GUIStyle.Orange + }; + var limitedString = ToolBox.LimitString(containerTagText.Text, containerTagText.Font, itemEditor.Rect.Width - containerTagButtonLayout.Rect.Width); + if (limitedString != containerTagText.Text) + { + containerTagText.ToolTip = containerTagText.Text; + containerTagText.Text = limitedString; + } + itemEditor.AddCustomContent(containerTagLayout, 3); + } var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { @@ -972,6 +1000,12 @@ namespace Barotrauma }; itemEditor.AddCustomContent(tickBox, 1); } + + if (!Layer.IsNullOrEmpty()) + { + var layerText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)) { MinSize = new Point(0, heightScaled) }, TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer)); + itemEditor.AddCustomContent(layerText, 1); + } } foreach (ItemComponent ic in components) @@ -987,7 +1021,7 @@ namespace Barotrauma } else { - if (ic.requiredItems.Count == 0 && ic.DisabledRequiredItems.Count == 0 && SerializableProperty.GetProperties(ic).Count == 0) { continue; } + if (ic.RequiredItems.Count == 0 && ic.DisabledRequiredItems.Count == 0 && SerializableProperty.GetProperties(ic).Count == 0) { continue; } } new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); @@ -1004,7 +1038,7 @@ namespace Barotrauma } List requiredItems = new List(); - foreach (var kvp in ic.requiredItems) + foreach (var kvp in ic.RequiredItems) { foreach (RelatedItem relatedItem in kvp.Value) { @@ -1089,34 +1123,389 @@ namespace Barotrauma return result; } - private void CreateTagPicker(GUITextBox textBox, IEnumerable availableTags) + public void CreateContainerTagPicker([MaybeNull] GUITextBox tagTextBox) { - var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox(string.Empty, string.Empty, new[] { TextManager.Get("Ok") }, new Vector2(0.35f, 0.6f), new Point(400, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; - var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) + var infoIcon = new GUIImage(new RectTransform(new Vector2(0.066f), msgBox.InnerFrame.RectTransform) { - PlaySoundOnSelect = true, - OnSelected = (component, userData) => + RelativeOffset = new Vector2(0.015f) + }, style: "GUIButtonInfo") + { + ToolTip = TextManager.Get("containertagui.tutorial"), + IgnoreLayoutGroups = true + }; + + var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.85f), msgBox.Content.RectTransform)); + + var list = new GUIListBox(new RectTransform(new Vector2(1f, 1f), layout.RectTransform)); + + const float NameSize = 0.4f; + const float ItemSize = 0.5f; + const float CountSize = 0.1f; + + var headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), isHorizontal: true); + new GUIButton(new RectTransform(new Vector2(NameSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.tag"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + new GUIButton(new RectTransform(new Vector2(ItemSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.items"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + new GUIButton(new RectTransform(new Vector2(CountSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.count"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + + var itemsByTag = + ContainerTagPrefab.Prefabs + .ToImmutableDictionary( + ct => ct, + ct => ct.GetItemsAndSpawnProbabilities()); + + // Group the prefabs by category and turn them into a dictionary where the key is the category and value is the list of identifiers of the prefabs. + // LINQ GroupBy returns GroupedEnumerable where the enumerable is the list of prefabs and key is what we grouped by. + var tagCategories = ContainerTagPrefab.Prefabs + .GroupBy(ct => ct.Category) + .ToImmutableDictionary( + g => g.Key, + g => g.Select(ct => ct.Identifier).ToImmutableArray()); + + foreach (var (category, categoryTags) in tagCategories) + { + var categoryButton = new GUIButton(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), style: "GUIButtonSmallFreeScale"); + categoryButton.Color *= 0.66f; + var categoryLayout = new GUILayoutGroup(new RectTransform(Vector2.One, categoryButton.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var categoryText = new GUITextBlock(new RectTransform(Vector2.One, categoryLayout.RectTransform), TextManager.Get($"tagcategory.{category}"), font: GUIStyle.SubHeadingFont); + var arrowImage = new GUIImage(new RectTransform(new Vector2(1f, 0.5f), categoryLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrowFreeScale"); + var arrowPadding = new GUIFrame(new RectTransform(new Vector2(0.025f, 1f), categoryLayout.RectTransform), style: null); + + bool hasHiddenCategories = false; + foreach (var categoryTag in categoryTags.OrderBy(t => t.Value)) { - if (!(userData is Identifier)) { return true; } - AddTag((Identifier)userData); - textBox.Text = Tags; - msgBox.Close(); + var found = itemsByTag.FirstOrNull(kvp => kvp.Key.Identifier == categoryTag); + if (found is null) + { + DebugConsole.ThrowError($"Failed to find tag with identifier {categoryTag} in itemsByTag"); + continue; + } + + var (tag, prefabsAndProbabilities) = found.Value; + + bool isCorrectSubType = tag.IsRecommendedForSub(Submarine); + + var tagLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), list.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + UserData = category, + Visible = isCorrectSubType + }; + + if (!isCorrectSubType) + { + hasHiddenCategories = true; + } + + var checkBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(NameSize, 1f), tagLayout.RectTransform), childAnchor: Anchor.Center); + var enabledCheckBox = new GUITickBox(new RectTransform(Vector2.One, checkBoxLayout.RectTransform, Anchor.Center), tag.Name, font: GUIStyle.SmallFont) + { + Selected = tags.Contains(tag.Identifier), + ToolTip = tag.Description + }; + + var tickBoxText = enabledCheckBox.TextBlock; + tickBoxText.Text = ToolBox.LimitString(tickBoxText.Text, tickBoxText.Font, tickBoxText.Rect.Width); + + var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(ItemSize, 1f), tagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var itemLayoutScissor = new GUIScissorComponent(new RectTransform(new Vector2(0.8f, 1f), itemLayout.RectTransform)) { CanBeFocused = false }; + var itemLayoutButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1), itemLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); + var itemLayoutButton = new GUIButton(new RectTransform(new Vector2(0.8f), itemLayoutButtonLayout.RectTransform), text: "...", style: "GUICharacterInfoButton") + { + UserData = tag, + ToolTip = TextManager.Get("containertagui.viewprobabilities") + }; + + itemLayoutButtonLayout.Recalculate(); + + float scroll = 0f; + float localScroll = 0f; + int lastSkippedItems = 0; + int skippedItems = 0; + var itemLayoutDraw = new GUICustomComponent(new RectTransform(new Vector2(1f, 0.9f), itemLayoutScissor.Content.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) => + { + component.ToolTip = string.Empty; + + const float padding = 8f; + float offset = 0f; + float size = component.Rect.Height; + int start = (int)Math.Floor(scroll); + int amountToDraw = (int)Math.Ceiling(component.Rect.Width / size) + 1; // +1 just to be on the safe side + bool shouldIncrementOnSkip = true; + float toDrawWidth = prefabsAndProbabilities.Length * (size + padding); + + // if the width is less than the component width we need to limit how many items we draw or it looks weird + if (toDrawWidth < component.Rect.Width) + { + shouldIncrementOnSkip = false; + amountToDraw = prefabsAndProbabilities.Length; + } + + for (int i = start; i < start + amountToDraw; i++) + { + var (ip, probability, _) = prefabsAndProbabilities[i % prefabsAndProbabilities.Length]; + var sprite = ip.InventoryIcon ?? ip.Sprite; + + if (sprite is null) + { + // I don't think this should happen but just in case + if (shouldIncrementOnSkip) + { + amountToDraw++; + skippedItems++; + } + continue; + } + + if (ShouldHideItemPrefab(ip, probability)) + { + if (shouldIncrementOnSkip) + { + skippedItems++; + amountToDraw++; + } + continue; + } + + float partialScroll = localScroll * (size + padding); + var drawRect = new RectangleF(itemLayoutScissor.Rect.X + offset - partialScroll, component.Rect.Y, size, size); + + var isMouseOver = drawRect.Contains(PlayerInput.MousePosition); + if (isMouseOver) + { + component.ToolTip = ip.CreateTooltipText(); + } + + var slotSprite = Inventory.SlotSpriteSmall; + slotSprite?.Draw(spriteBatch, drawRect.Location, Color.White, origin: Vector2.Zero, rotate: 0f, scale: size / slotSprite.size.X * Inventory.SlotSpriteSmallScale); + + float iconScale = Math.Min(drawRect.Width / sprite.size.X, drawRect.Height / sprite.size.Y) * 0.9f; + + Color drawColor = ip.InventoryIconColor; + + sprite.Draw(spriteBatch, drawRect.Center, drawColor, origin: sprite.Origin, scale: iconScale); + offset += size + padding; + } + + // we need to compensate for the skipped items so that the scroll doesn't jump around + if (skippedItems < lastSkippedItems) + { + scroll += lastSkippedItems - skippedItems; + } + + lastSkippedItems = skippedItems; + skippedItems = 0; + }, onUpdate: (deltaTime, component) => + { + if (GUI.MouseOn != component && MathUtils.NearlyEqual(localScroll, 0, deltaTime * 2)) + { + localScroll = 0f; + return; + } + + float totalWidth = prefabsAndProbabilities.Length * (component.Rect.Height + 8f); + if (totalWidth < component.Rect.Width) { return; } + scroll += deltaTime; + localScroll = scroll % 1f; + }) + { + HoverCursor = CursorState.Default, + AlwaysOverrideCursor = true + }; + + var tooltip = TextManager.Get(tag.WarnIfLess ? "ContainerTagUI.RecommendedAmount" : "ContainerTagUI.SuggestedAmount"); + + var countBlock = new GUITextBlock(new RectTransform(new Vector2(CountSize, 1f), tagLayout.RectTransform), string.Empty, textAlignment: Alignment.Center) + { + ToolTip = tooltip + }; + UpdateCountBlock(countBlock, tag); + + enabledCheckBox.OnSelected += tickBox => + { + if (tickBox.Selected) + { + AddTag(tag.Identifier); + } + else + { + RemoveTag(tag.Identifier); + } + + if (tagTextBox is not null) + { + tagTextBox.Text = string.Join(',', tags.Where(t => !Prefab.Tags.Contains(t))); + } + UpdateCountBlock(countBlock, tag); + return true; + }; + + itemLayoutButton.OnClicked = (button, _) => + { + CreateContainerTagItemListPopup(tag, button.Rect.Center, layout, prefabsAndProbabilities); + return true; + }; + + void UpdateCountBlock(GUITextBlock textBlock, ContainerTagPrefab containerTag) + { + if (textBlock is null) { return; } + + var tagCount = Submarine.GetItems(alsoFromConnectedSubs: true).Count(i => i.HasTag(containerTag.Identifier)); + textBlock.Text = $"{tagCount} ({containerTag.RecommendedAmount})"; + + if (!isCorrectSubType || !containerTag.WarnIfLess || containerTag.RecommendedAmount <= 0) { return; } + + if (tagCount < containerTag.RecommendedAmount) + { + textBlock.TextColor = GUIStyle.Red; + textBlock.Text += "*"; + textBlock.ToolTip = RichString.Rich($"{tooltip}\n\n‖color:gui.red‖{TextManager.Get("ContainerTagUI.RecommendedAmountWarning")}‖color:end‖"); + } + else if (tagCount >= containerTag.RecommendedAmount) + { + textBlock.TextColor = GUIStyle.Green; + textBlock.ToolTip = tooltip; + } + } + } + + arrowImage.SpriteEffects = hasHiddenCategories ? SpriteEffects.None : SpriteEffects.FlipVertically; + categoryButton.OnClicked = (_, _) => + { + arrowImage.SpriteEffects ^= SpriteEffects.FlipVertically; + + foreach (var child in list.Content.Children) + { + if (child.UserData is Identifier id && id == category) + { + child.Visible = !child.Visible; + } + } + return true; + }; + } + } + + private static void CreateContainerTagItemListPopup(ContainerTagPrefab tag, Point location, GUIComponent popupParent, ImmutableArray prefabAndProbabilities) + { + const string TooltipUserData = "tooltip"; + const string ProbabilityUserData = "probability"; + + if (popupParent.GetChildByUserData(TooltipUserData) is { } existingTooltip) + { + popupParent.RemoveChild(existingTooltip); + } + + var tooltip = new GUIFrame(new RectTransform(new Point(popupParent.Rect.Height), popupParent.RectTransform) + { + AbsoluteOffset = location - popupParent.Rect.Location + }) + { + UserData = TooltipUserData, + IgnoreLayoutGroups = true + }; + + if (tooltip.Rect.Bottom > GameMain.GraphicsHeight) + { + int diffY = tooltip.Rect.Bottom - GameMain.GraphicsHeight; + tooltip.RectTransform.AbsoluteOffset -= new Point(0, diffY); + } + + if (tooltip.Rect.Right > GameMain.GraphicsWidth) + { + int diffX = tooltip.Rect.Right - GameMain.GraphicsWidth; + tooltip.RectTransform.AbsoluteOffset -= new Point(diffX, 0); + } + + var tooltipLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(tooltip.RectTransform, 0.9f), tooltip.RectTransform, Anchor.Center)); + + var tooltipHeader = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), tag.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + var tooltipList = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), tooltipLayout.RectTransform)); + + var tooltipHeaderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true); + new GUIButton(new RectTransform(new Vector2(0.66f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.item"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + new GUIButton(new RectTransform(new Vector2(0.33f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.probability"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; + + foreach (var itemAndProbability in prefabAndProbabilities.OrderByDescending(p => p.Probability)) + { + var (ip, probability, campaignOnlyProbability) = itemAndProbability; + if (ShouldHideItemPrefab(ip, probability)) { continue; } + + var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + UserData = itemAndProbability + }; + + var itemNameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), itemLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true + }; + + var itemIcon = new GUIImage(new RectTransform(Vector2.One, itemNameLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) + { + Color = ip.InventoryIconColor + }; + + var itemName = new GUITextBlock(new RectTransform(Vector2.One, itemNameLayout.RectTransform), ip.Name); + itemName.Text = ToolBox.LimitString(ip.Name, itemName.Font, itemName.Rect.Width); + + var toolTipContainer = new GUIFrame(new RectTransform(Vector2.One, itemNameLayout.RectTransform), style: null) + { + IgnoreLayoutGroups = true, + ToolTip = ip.CreateTooltipText() + }; + + var probabilityText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), itemLayout.RectTransform), ProbabilityToPercentage(campaignOnlyProbability), textAlignment: Alignment.Right) + { + UserData = ProbabilityUserData + }; + if (MathUtils.NearlyEqual(campaignOnlyProbability, 0f)) { probabilityText.TextColor = GUIStyle.Red; } + } + + var campaignCheckbox = new GUITickBox(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), label: TextManager.Get("containertagui.campaignonly")) + { + ToolTip = TextManager.Get("containertagui.campaignonlytooltip"), + Selected = true, + OnSelected = box => + { + foreach (var child in tooltipList.Content.Children) + { + if (child.UserData is not ContainerTagPrefab.ItemAndProbability data) { continue; } + + if (child.GetChildByUserData(ProbabilityUserData) is not GUITextBlock text) { continue; } + + float probability = box.Selected + ? data.CampaignProbability + : data.Probability; + text.Text = ProbabilityToPercentage(probability); + + text.TextColor = MathUtils.NearlyEqual(probability, 0f) + ? GUIStyle.Red + : GUIStyle.TextColorNormal; + } + return true; } }; - foreach (var availableTag in availableTags.ToList().OrderBy(t => t)) + var tooltipClose = new GUIButton(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), TextManager.Get("Close")) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, - ToolBox.LimitString(availableTag.Value, GUIStyle.Font, textList.Content.Rect.Width)) + OnClicked = (_, _) => { - UserData = availableTag - }; - } + popupParent.RemoveChild(tooltip); + return true; + } + }; + + static LocalizedString ProbabilityToPercentage(float probability) + => TextManager.GetWithVariable("percentageformat", "[value]", MathF.Round((probability * 100f), 1).ToString(CultureInfo.InvariantCulture)); + } + private static bool ShouldHideItemPrefab(ItemPrefab ip, float probability) + => ip.HideInMenus && MathUtils.NearlyEqual(probability, 0f); + /// /// Reposition currently active item interfaces to make sure they don't overlap with each other /// @@ -1213,10 +1602,21 @@ namespace Barotrauma } activeHUDs.Clear(); + maxPriorityHUDs.Clear(); + bool DrawHud(ItemComponent ic) + { + if (!ic.ShouldDrawHUD(character)) { return false; } + if (character.HasEquippedItem(this)) + { + return ic.DrawHudWhenEquipped; + } + else + { + return ic.CanBeSelected && ic.HasRequiredItems(character, addMessage: false); + } + } //the HUD of the component with the highest priority will be drawn //if all components have a priority of 0, all of them are drawn - maxPriorityHUDs.Clear(); - bool DrawHud(ItemComponent ic) => ic.ShouldDrawHUD(character) && (ic.CanBeSelected && ic.HasRequiredItems(character, addMessage: false) || (character.HasEquippedItem(this) && ic.DrawHudWhenEquipped)); foreach (ItemComponent ic in activeComponents) { if (ic.HudPriority > 0 && DrawHud(ic) && (maxPriorityHUDs.Count == 0 || ic.HudPriority >= maxPriorityHUDs[0].HudPriority)) @@ -1317,7 +1717,7 @@ namespace Barotrauma } } - readonly List texts = new List(); + readonly List texts = new(); public List GetHUDTexts(Character character, bool recreateHudTexts = true) { // Always create the texts if they have not yet been created @@ -1348,42 +1748,91 @@ namespace Barotrauma nameText += $" x{DroppedStack.Count()}"; } - texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, false, false)); + texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, isCommand: false, isError: false)); if (CampaignMode.BlocksInteraction(CampaignInteractionType)) { - texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, isCommand: false, isError: false)); } else { - foreach (ItemComponent ic in components) + foreach (ItemComponent itemComponent in components) { - if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } - if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } - if (ic is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { continue; } - Color color = Color.Gray; - if (ic.HasRequiredItems(character, false)) - { - if (ic is Repairable r) - { - if (r.IsBelowRepairThreshold) { color = Color.Cyan; } - } - else - { - color = Color.Cyan; - } - } - if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } - texts.Add(new ColoredText(ic.DisplayMsg.Value, color, false, false)); + var interactionVisibility = GetComponentInteractionVisibility(character, itemComponent); + if (interactionVisibility == InteractionVisibility.None) { continue; } + if (itemComponent.DisplayMsg.IsNullOrEmpty()) { continue; } + + Color color = interactionVisibility == InteractionVisibility.MissingRequirement ? Color.Gray : Color.Cyan; + texts.Add(new ColoredText(itemComponent.DisplayMsg.Value, color, isCommand: false, isError: false)); } } - if (PlayerInput.IsShiftDown() && CrewManager.DoesItemHaveContextualOrders(this)) + if (PlayerInput.KeyDown(InputType.ContextualCommand)) { - texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, isCommand: false, isError: false)); } + else + { + texts.Add(new ColoredText(TextManager.Get("itemmsg.morreoptionsavailable").Value, Color.LightGray * 0.7f, isCommand: false, isError: false)); + } return texts; } + private enum InteractionVisibility + { + None, + MissingRequirement, + Visible + } + + /// + /// Determine, for UI display purposes, the type of interaction visibility for an item component. + /// + /// Example: + /// Visible -> Display cyan "click to interact" type text on item hover. + /// MissingRequirement -> Display gray "need tool" type text on item hover. + /// None -> Hide from item hover texts. + /// + /// Character, for tool requirement purposes. + /// The item component to inspect. + /// The interaction visibility state for this component. + private static InteractionVisibility GetComponentInteractionVisibility(Character character, ItemComponent itemComponent) + { + if (!itemComponent.CanBePicked && !itemComponent.CanBeSelected) { return InteractionVisibility.None; } + if (itemComponent is Holdable holdable && !holdable.CanBeDeattached()) { return InteractionVisibility.None; } + if (itemComponent is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { return InteractionVisibility.None; } + + InteractionVisibility interactionVisibility = InteractionVisibility.MissingRequirement; + if (itemComponent.HasRequiredItems(character, addMessage: false)) + { + if (itemComponent is Repairable repairable) + { + if (repairable.IsBelowRepairThreshold) + { + interactionVisibility = InteractionVisibility.Visible; + } + } + else + { + interactionVisibility = InteractionVisibility.Visible; + } + } + + return interactionVisibility; + } + + public bool HasVisibleInteraction(Character character) + { + foreach (var component in components) + { + if (GetComponentInteractionVisibility(character, component) == InteractionVisibility.Visible) + { + return true; + } + } + + return false; + } + public void ForceHUDLayoutUpdate(bool ignoreLocking = false) { foreach (ItemComponent ic in activeHUDs) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 362dafa31..e12c9b41a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -331,7 +331,6 @@ namespace Barotrauma } private GUIComponent CreateEditingHUD(bool inGame = false) { - editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index a3ab80804..d2deb27cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -170,16 +170,18 @@ namespace Barotrauma { if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) { - GameMain.NetworkMember?.CreateEntityEvent(this, new StatusEventData()); + //these are used to modify the amount water/fire in the hull with console commands + //they should be usable even when not controlling a character + GameMain.Client?.CreateEntityEvent(this, new StatusEventData(), requireControlledCharacter: false); } foreach (Decal decal in pendingDecalUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new DecalEventData(decal)); + GameMain.Client?.CreateEntityEvent(this, new DecalEventData(decal)); } pendingDecalUpdates.Clear(); foreach (int pendingSectionUpdate in pendingSectionUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); + GameMain.Client?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } pendingSectionUpdates.Clear(); networkUpdatePending = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 9f9c06073..5375c89eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -16,27 +16,34 @@ namespace Barotrauma float scale = Math.Min(drawArea.Width / (float)Bounds.Width, drawArea.Height / (float)Bounds.Height) * 0.9f; - foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) + foreach (var displayEntity in DisplayEntities) { - var entityPrefab = FindByIdentifier(identifier); + var entityPrefab = FindByIdentifier(displayEntity.Identifier); if (entityPrefab is CoreEntityPrefab || entityPrefab == null) { continue; } var drawRect = new Rectangle( - (int)(rect.X * scale) + drawArea.Center.X, (int)((rect.Y) * scale) - drawArea.Center.Y, - (int)(rect.Width * scale), (int)(rect.Height * scale)); - entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale * scale); + (int)(displayEntity.Rect.X * scale) + drawArea.Center.X, (int)((displayEntity.Rect.Y) * scale) - drawArea.Center.Y, + (int)(displayEntity.Rect.Width * scale), (int)(displayEntity.Rect.Height * scale)); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale * scale, rotation: displayEntity.RotationRad); } } public override void DrawPlacing(SpriteBatch spriteBatch, Camera cam) { base.DrawPlacing(spriteBatch, cam); - foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) + Draw( + spriteBatch, + placePosition != Vector2.Zero ? placePosition : Submarine.MouseToWorldGrid(cam, Submarine.MainSub)); + } + + public void Draw(SpriteBatch spriteBatch, Vector2 pos) + { + foreach (var displayEntity in DisplayEntities) { - var entityPrefab = FindByIdentifier(identifier); + var entityPrefab = FindByIdentifier(displayEntity.Identifier); if (entityPrefab == null) { continue; } - Rectangle drawRect = rect; - drawRect.Location += placePosition != Vector2.Zero ? placePosition.ToPoint() : Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); - entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale); + Rectangle drawRect = displayEntity.Rect; + drawRect.Location += pos.ToPoint(); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale, rotation: displayEntity.RotationRad); } } @@ -47,9 +54,12 @@ namespace Barotrauma new XAttribute("description", description), new XAttribute("hideinmenus", hideInMenus)); - //move the entities so that their "center of mass" is at {0,0} var assemblyEntities = MapEntity.CopyEntities(entities); + for (int i = 0; i < assemblyEntities.Count && i < entities.Count; i++) + { + assemblyEntities[i].Layer = entities[i].Layer; + } //find wires and items that are contained inside another item //place them at {0,0} to prevent them from messing up the origin of the prefab and to hide them in preview diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs index a137242ce..75991e4b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs @@ -20,6 +20,8 @@ namespace Barotrauma { if (damage <= 0.0f) { return; } Vector2 particlePos = worldPosition; + Vector2 particleDir = particlePos - WorldPosition; + if (particleDir.LengthSquared() > 0.0001f) { particleDir = Vector2.Normalize(particleDir); } if (!Cells.Any(c => c.IsPointInside(particlePos))) { bool intersectionFound = false; @@ -31,6 +33,7 @@ namespace Barotrauma { intersectionFound = true; particlePos = intersection; + particleDir = edge.GetNormal(cell); break; } } @@ -38,14 +41,15 @@ namespace Barotrauma } } - Vector2 particleDir = particlePos - WorldPosition; - if (particleDir.LengthSquared() > 0.0001f) { particleDir = Vector2.Normalize(particleDir); } int particleAmount = MathHelper.Clamp((int)damage, 1, 10); for (int i = 0; i < particleAmount; i++) { - var particle = GameMain.ParticleManager.CreateParticle("iceshards", + var particle = GameMain.ParticleManager.CreateParticle("iceexplosionsmall", particlePos + Rand.Vector(5.0f), - particleDir * Rand.Range(200.0f, 500.0f) + Rand.Vector(100.0f)); + particleDir * Rand.Range(30.0f, 500.0f) + Rand.Vector(20.0f)); + GameMain.ParticleManager.CreateParticle("iceshards", + particlePos + Rand.Vector(5.0f), + particleDir * Rand.Range(100.0f, 500.0f) + Rand.Vector(100.0f)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 5fe2aeca2..34caf829f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -95,6 +95,7 @@ namespace Barotrauma } } } + foreach (var rects in blockedRects.Values) { foreach (var rect in rects) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 80027c367..1d9f4ecd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -18,7 +18,7 @@ namespace Barotrauma //Maximum number of visible objects drawn at once. Should be large enough to not have an effect during normal gameplay, //but small enough to prevent wrecking performance when zooming out very far - const int MaxVisibleObjects = 500; + const int MaxVisibleObjects = 600; private Rectangle currentGridIndices; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 9366776a9..8b6a6b6fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -362,6 +362,13 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(edge.Point1.X + cell.Translation.X, -(edge.Point1.Y + cell.Translation.Y)), new Vector2(edge.Point2.X + cell.Translation.X, -(edge.Point2.Y + cell.Translation.Y)), edge.NextToCave ? Color.Red : (cell.Body == null ? Color.Cyan * 0.5f : (edge.IsSolid ? Color.White : Color.Gray)), width: edge.NextToCave ? 8 : 1); + + Vector2 normal = edge.GetNormal(cell); + GUI.DrawLine(spriteBatch, + (edge.Center + cell.Translation).FlipY(), + (edge.Center + cell.Translation + normal * 32).FlipY(), + Color.Red * 0.5f, + width: 3); } foreach (Vector2 point in cell.BodyVertices) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index bb2024258..31c08ce71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -156,17 +156,7 @@ namespace Barotrauma.Lights BoundingBox = rect; this.isHorizontal = isHorizontal; - if (ParentEntity is Structure structure) - { - Debug.Assert(!structure.Removed); - isHorizontal = structure.IsHorizontal; - } - else if (ParentEntity is Item item) - { - Debug.Assert(!item.Removed); - var door = item.GetComponent(); - if (door != null) { isHorizontal = door.IsHorizontal; } - } + Debug.Assert(!ParentEntity.Removed); Vector2[] verts = new Vector2[] { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 4c789dcf7..2a60446e1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -695,12 +695,24 @@ namespace Barotrauma.Lights if (diff.LengthSquared() > 20.0f * 20.0f) { losOffset = diff; } float rotation = MathUtils.VectorToAngle(losOffset); + //the visible area stretches to the maximum when the cursor is this far from the character + const float MaxOffset = 256.0f; + const float MinHorizontalScale = 2.2f; + const float MaxHorizontalScale = 2.8f; + const float VerticalScale = 2.5f; + + //Starting point and scale-based modifier that moves the point of origin closer to the edge of the texture if the player moves their mouse further away, or vice versa. + float relativeOriginStartPosition = 0.22f; //Increasing this value moves the origin further behind the character + float originStartPosition = visionCircle.Width * relativeOriginStartPosition; + float relativeOriginLookAtPosModifier = -0.055f; //Increase this value increases how much the vision changes by moving the mouse + float originLookAtPosModifier = visionCircle.Width * relativeOriginLookAtPosModifier; + Vector2 scale = new Vector2( - MathHelper.Clamp(losOffset.Length() / 256.0f, 4.0f, 5.0f), 3.0f); + MathHelper.Clamp(losOffset.Length() / MaxOffset, MinHorizontalScale, MaxHorizontalScale), VerticalScale); spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f))); spriteBatch.Draw(visionCircle, new Vector2(ViewTarget.WorldPosition.X, -ViewTarget.WorldPosition.Y), null, Color.White, rotation, - new Vector2(visionCircle.Width * 0.2f, visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); + new Vector2(originStartPosition + (scale.X * originLookAtPosModifier), visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); spriteBatch.End(); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 92beaecdb..74eba1d6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -461,7 +461,9 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) => { if (location.Reputation == null) { return; } - RoundSummary.DrawReputationBar(sb, component.Rect, location.Reputation.NormalizedValue); + RoundSummary.DrawReputationBar(sb, component.Rect, + location.Reputation.NormalizedValue, + location.Reputation.MinReputation, location.Reputation.MaxReputation); }); new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), repBarHolder.RectTransform), @@ -1128,8 +1130,11 @@ namespace Barotrauma if (connection.LevelData.HasBeaconStation) { - var beaconStationIconStyle = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; - DrawIcon(beaconStationIconStyle, (int)(28 * zoom), connection.LevelData.IsBeaconActive ? beaconStationActiveText : beaconStationInactiveText); + bool beaconActive = + connection.LevelData.IsBeaconActive || + (Level.Loaded?.LevelData == connection.LevelData && Level.Loaded.CheckBeaconActive()); + var beaconStationIconStyle = beaconActive ? "BeaconStationActive" : "BeaconStationInactive"; + DrawIcon(beaconStationIconStyle, (int)(28 * zoom), beaconActive ? beaconStationActiveText : beaconStationInactiveText); } if (connection.Locked) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index bfb858f90..6560a4ea1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -808,8 +808,16 @@ namespace Barotrauma { if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; } if (structure.FlippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; } - spriteRotation = MathHelper.ToRadians(structure.Rotation); - rectangleRotation = spriteRotation; + rectangleRotation = MathHelper.ToRadians(structure.Rotation); + + spriteRotation = rectangleRotation; + bool spriteIsFlippedHorizontally = structure.Sprite.effects.HasFlag(SpriteEffects.FlipHorizontally); + bool spriteIsFlippedVertically = structure.Sprite.effects.HasFlag(SpriteEffects.FlipVertically); + if (spriteIsFlippedHorizontally != spriteIsFlippedVertically) + { + spriteRotation = -spriteRotation; + } + if (structure.FlippedX != structure.FlippedY) { rectangleRotation = -rectangleRotation; } break; } @@ -978,6 +986,11 @@ namespace Barotrauma } } + public static void ResetEditingHUD() + { + editingHUD = null; + } + public static void DrawEditor(SpriteBatch spriteBatch, Camera cam) { if (SelectedList.Count == 1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index 5007fbecc..dfb0f0e1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -1,12 +1,32 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; namespace Barotrauma { abstract partial class MapEntityPrefab : PrefabWithUintIdentifier { + public RichString CreateTooltipText() + { + LocalizedString name = Category.HasFlag(MapEntityCategory.Legacy) ? TextManager.GetWithVariable("legacyitemformat", "[name]", Name) : Name; + LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; + + if (!Description.IsNullOrEmpty()) + { + tooltip += '\n' + Description; + } + + if (IsModded) + { + tooltip = $"{tooltip}\n‖color:{Color.MediumPurple.ToStringHex()}‖{ContentPackage?.Name}‖color:end‖"; + } + + return RichString.Rich(tooltip); + } + + public bool IsModded + => ContentPackage != GameMain.VanillaContent && ContentPackage != null; + public virtual void UpdatePlacing(Camera cam) { if (PlayerInput.SecondaryMouseButtonClicked()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs index 1b3c99d28..ca438606f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -25,8 +25,10 @@ namespace Barotrauma Stream = sound.Stream; Range = element.GetAttributeFloat("range", 1000.0f); Volume = element.GetAttributeFloat("volume", 1.0f); + IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); + FrequencyMultiplierRange = new Vector2(1.0f); - string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0"))!; + string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0")); if (!freqMultAttr.Contains(',')) { if (float.TryParse(freqMultAttr, NumberStyles.Any, CultureInfo.InvariantCulture, out float freqMult)) @@ -47,7 +49,6 @@ namespace Barotrauma DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")", contentPackage: element.ContentPackage); } - IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); } public float GetRandomFrequencyMultiplier() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index b14536cc1..1db65ab0d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -221,6 +221,12 @@ namespace Barotrauma editor.AddCustomContent(tickBox, 1); } + if (!Layer.IsNullOrEmpty()) + { + var layerText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)) { MinSize = new Point(0, heightScaled) }, TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer)); + editor.AddCustomContent(layerText, 1); + } + var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { Stretch = true, @@ -435,13 +441,12 @@ namespace Barotrauma dropShadowOffset.Y = -dropShadowOffset.Y; } - SpriteEffects oldEffects = Prefab.BackgroundSprite.effects; - Prefab.BackgroundSprite.effects ^= SpriteEffects; - Vector2 backGroundOffset = new Vector2( MathUtils.PositiveModulo(-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width * TextureScale.X * Scale), MathUtils.PositiveModulo(-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height * TextureScale.Y * Scale)); + float rotationRad = rotationForSprite(this.rotationRad, Prefab.BackgroundSprite); + Prefab.BackgroundSprite.DrawTiled( spriteBatch, new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)), @@ -451,7 +456,8 @@ namespace Barotrauma color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, - depth: Math.Max(GetDrawDepth(Prefab.BackgroundSprite.Depth, Prefab.BackgroundSprite), depth + 0.000001f)); + depth: Math.Max(GetDrawDepth(Prefab.BackgroundSprite.Depth, Prefab.BackgroundSprite), depth + 0.000001f), + spriteEffects: Prefab.BackgroundSprite.effects ^ SpriteEffects); if (UseDropShadow) { @@ -464,18 +470,14 @@ namespace Barotrauma color: Color.Black * 0.5f, textureScale: TextureScale * Scale, startOffset: backGroundOffset, - depth: (depth + Prefab.BackgroundSprite.Depth) / 2.0f); + depth: (depth + Prefab.BackgroundSprite.Depth) / 2.0f, + spriteEffects: Prefab.BackgroundSprite.effects ^ SpriteEffects); } - - Prefab.BackgroundSprite.effects = oldEffects; } } if (back == GetRealDepth() > 0.5f) { - SpriteEffects oldEffects = Prefab.Sprite.effects; - Prefab.Sprite.effects ^= SpriteEffects; - Vector2 advanceX = MathUtils.RotatedUnitXRadians(this.rotationRad).FlipY(); Vector2 advanceY = advanceX.YX().FlipX(); if (FlippedX != FlippedY) @@ -483,6 +485,9 @@ namespace Barotrauma advanceX = advanceX.FlipY(); advanceY = advanceY.FlipX(); } + + float sectionSpriteRotationRad = rotationForSprite(this.rotationRad, Prefab.Sprite); + for (int i = 0; i < Sections.Length; i++) { Rectangle drawSection = Sections[i].rect; @@ -532,12 +537,13 @@ namespace Barotrauma spriteBatch, pos, new Vector2(drawSection.Width, drawSection.Height), - rotation: rotationRad, + rotation: sectionSpriteRotationRad, origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: sectionOffset, depth: depth, - textureScale: TextureScale * Scale); + textureScale: TextureScale * Scale, + spriteEffects: Prefab.Sprite.effects ^ SpriteEffects); } foreach (var decorativeSprite in Prefab.DecorativeSprites) @@ -545,27 +551,42 @@ namespace Barotrauma if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor) + this.rotationRad; Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, Prefab.Sprite.effects, + Vector2 drawPos = DrawPosition + MathUtils.RotatePoint(offset, -this.rotationRad); + decorativeSprite.Sprite.Draw( + spriteBatch: spriteBatch, + pos: drawPos.FlipY(), + color: color, + rotate: rotation, + scale: decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, + spriteEffect: Prefab.Sprite.effects ^ SpriteEffects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - Prefab.Sprite.Depth), 0.999f)); } - Prefab.Sprite.effects = oldEffects; + } + + static float rotationForSprite(float rotationRad, Sprite sprite) + { + if (sprite.effects.HasFlag(SpriteEffects.FlipHorizontally) != sprite.effects.HasFlag(SpriteEffects.FlipVertically)) + { + rotationRad = -rotationRad; + } + return rotationRad; } if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.5f) { if (Bodies != null) { - for (int i = 0; i < Bodies.Count; i++) + foreach (var body in Bodies) { - Vector2 pos = FarseerPhysics.ConvertUnits.ToDisplayUnits(Bodies[i].Position); + Vector2 pos = ConvertUnits.ToDisplayUnits(body.Position); if (Submarine != null) { pos += Submarine.DrawPosition; } pos.Y = -pos.Y; + var dimensions = bodyDimensions[body]; GUI.DrawRectangle(spriteBatch, pos, - FarseerPhysics.ConvertUnits.ToDisplayUnits(bodyDebugDimensions[i].X), - FarseerPhysics.ConvertUnits.ToDisplayUnits(bodyDebugDimensions[i].Y), - -Bodies[i].Rotation, Color.White); + ConvertUnits.ToDisplayUnits(dimensions.X), + ConvertUnits.ToDisplayUnits(dimensions.Y), + -body.Rotation, Color.White); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 9cf47521f..0b4494a0d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text; namespace Barotrauma { @@ -506,6 +507,47 @@ namespace Barotrauma Hull.ShowHulls = true; } + if (!IsWarningSuppressed(SubEditorScreen.WarningType.NotEnoughContainers)) + { + HashSet missingContainerTags = new(); + foreach (var prefab in ContainerTagPrefab.Prefabs) + { + if (!prefab.IsRecommendedForSub(this) || !prefab.WarnIfLess) { continue; } + + int count = Item.ItemList.Count(i => i.HasTag(prefab.Identifier)); + if (count < prefab.RecommendedAmount) + { + missingContainerTags.Add(prefab); + } + } + + if (missingContainerTags.Any()) + { + StringBuilder sb = new(); + int count = 0; + foreach (var tag in missingContainerTags) + { + sb.AppendLine($"- {tag.Name}"); + count++; + if (missingContainerTags.Count > count && count >= 3) + { + var moreIndicator = TextManager.GetWithVariable( + "upgradeuitooltip.moreindicator", + "[amount]", + (missingContainerTags.Count - count).ToString()).Value; + sb.AppendLine(moreIndicator); + break; + } + } + + errorMsgs.Add(TextManager.GetWithVariable( + "ContainerTagUI.CountWarning", + "[tags]", + sb.ToString()).Value); + warnings.Add(SubEditorScreen.WarningType.NotEnoughContainers); + } + } + if (Info.Type == SubmarineType.Player) { foreach (Item item in Item.ItemList) @@ -521,7 +563,40 @@ namespace Barotrauma break; } } + foreach (Item item in Item.ItemList) + { + if (item.GetComponent() is not OxygenGenerator oxygenGenerator) { continue; } + Dictionary hullOxygenFlow = new Dictionary(); + + foreach (var linkedTo in item.linkedTo) + { + if (linkedTo is not Item linkedItem || linkedItem.GetComponent() is not Vent vent) { continue; } + if (vent.Item.CurrentHull == null) + { + vent.Item.FindHull(); + if (vent.Item.CurrentHull == null) { continue; } + } + float oxygenFlow = oxygenGenerator.GetVentOxygenFlow(vent); + if (!hullOxygenFlow.ContainsKey(vent.Item.CurrentHull)) + { + hullOxygenFlow[vent.Item.CurrentHull] = oxygenFlow; + } + else + { + hullOxygenFlow[vent.Item.CurrentHull] += oxygenFlow; + } + } + foreach ((Hull hull, float oxygenFlow) in hullOxygenFlow) + { + if (oxygenFlow < Hull.OxygenConsumptionSpeed) + { + errorMsgs.Add(TextManager.GetWithVariable("LowOxygenOutputWarning", "[roomname]", + hull.DisplayName).Value); + warnings.Add(SubEditorScreen.WarningType.LowOxygenOutputWarning); + } + } + } if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHumanSpawnpoints)) @@ -554,6 +629,15 @@ namespace Barotrauma warnings.Add(SubEditorScreen.WarningType.NoHiddenContainers); } } + if (Info.Dimensions.X * Physics.DisplayToRealWorldRatio > 80 || + Info.Dimensions.Y * Physics.DisplayToRealWorldRatio > 32) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooLargeForEndGame)) + { + errorMsgs.Add(TextManager.Get("TooLargeForEndGameWarning").Value); + warnings.Add(SubEditorScreen.WarningType.TooLargeForEndGame); + } + } } else if (Info.Type == SubmarineType.OutpostModule) { @@ -749,7 +833,9 @@ namespace Barotrauma public void ClientEventRead(IReadMessage msg, float sendingTime) { - throw new Exception($"Error while reading a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to receive events!"); + Identifier layerIdentifier = msg.ReadIdentifier(); + bool enabled = msg.ReadBoolean(); + SetLayerEnabled(layerIdentifier, enabled); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs index 27242fb85..145f55d0e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs @@ -1,12 +1,52 @@ using FarseerPhysics; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; +using Voronoi2; namespace Barotrauma { partial class SubmarineBody { + + partial void HandleLevelCollisionProjSpecific(Impact impact) + { + float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal); + int particleAmount = (int)Math.Min(wallImpact, 10); + + const float BurstParticleThreshold = 5.0f; + + float velocityFactor = MathHelper.Clamp(wallImpact / 10.0f, 0.0f, 1.0f); + for (int i = 0; i < particleAmount * 5; i++) + { + GameMain.ParticleManager.CreateParticle("iceshards", + ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), + (Rand.Vector(0.9f) + impact.Normal) * Rand.Range(100.0f, 10000) * velocityFactor); + } + for (int i = 0; i < particleAmount; i++) + { + float particleVelocityMultiplier = Rand.Range(0.0f, 1); + var p = GameMain.ParticleManager.CreateParticle("iceexplosion", + ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), + (Rand.Vector(0.5f) + impact.Normal) * particleVelocityMultiplier * 500 * velocityFactor); + if (p != null) + { + p.VelocityChangeMultiplier = particleVelocityMultiplier * Rand.Range(0.0f, 1.0f); + p.Size *= Math.Max(particleVelocityMultiplier, 0); + } + } + if (wallImpact > BurstParticleThreshold) + { + for (int i = 0; i < particleAmount; i++) + { + GameMain.ParticleManager.CreateParticle("iceburst", + ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), + angle: MathUtils.VectorToAngle(impact.Normal.FlipY() + Rand.Vector(0.25f)), speed: 0.0f); + } + } + } + partial void ClientUpdatePosition(float deltaTime) { if (GameMain.Client == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index d48f4c959..264d0aebc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -168,7 +168,7 @@ namespace Barotrauma private bool IsHidden() { - if (!SubEditorScreen.IsLayerVisible(this)) { return false; } + if (!SubEditorScreen.IsLayerVisible(this)) { return true; } if (spawnType == SpawnType.Path) { return (!GameMain.DebugDraw && !ShowWayPoints); @@ -316,6 +316,11 @@ namespace Barotrauma { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUIStyle.LargeFont); + if (!Layer.IsNullOrEmpty()) + { + var layerText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer)); + } + var spawnTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true) { Stretch = true, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index ad412a92b..82bb5ee62 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -46,7 +46,7 @@ namespace Barotrauma.Networking { if (localRemovedBans.Contains(bannedPlayer.UniqueIdentifier)) { continue; } - var playerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), ((GUIListBox)BanFrame).Content.RectTransform) { MinSize = new Point(0, 70) }) + var playerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), ((GUIListBox)BanFrame).Content.RectTransform) { MinSize = new Point(0, 70) }, style: "InnerFrame") { UserData = BanFrame }; @@ -80,18 +80,22 @@ namespace Barotrauma.Networking { CanBeFocused = true }; - textBlock.RectTransform.MinSize = new Point( - (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).X, 0); + textBlock.RectTransform.MinSize = new Point(0, (int)textBlock.Font.MeasureString(textBlock.Text.SanitizedValue).Y); - var removeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.4f), topArea.RectTransform), + var removeButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.4f), topArea.RectTransform, Anchor.CenterRight), TextManager.Get("BanListRemove"), style: "GUIButtonSmall") { + IgnoreLayoutGroups = true, UserData = bannedPlayer, - OnClicked = RemoveBan + OnClicked = RemoveBan, + Enabled = false }; - topArea.RectTransform.MinSize = new Point(0, (int)(removeButton.Rect.Height * 1.25f)); - - topArea.ForceLayoutRecalculation(); + removeButton.OnAddedToGUIUpdateList += (component) => + { + component.Enabled = GameMain.Client?.HasPermission(ClientPermissions.Unban) ?? false; + }; + topArea.RectTransform.MinSize = new Point(0, Math.Max(textBlock.RectTransform.MinSize.Y, removeButton.RectTransform.MinSize.Y)); + topArea.RectTransform.IsFixedSize = true; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), bannedPlayer.ExpirationTime.TryUnwrap(out var expirationTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 318176714..4d31e3c57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -14,6 +14,17 @@ namespace Barotrauma.Networking set; } + // Players can boost per-user volume by 200% + public const float MaxVoiceChatBoost = 2.0f; + + private float voiceVolume = 1f; + + public float VoiceVolume + { + get => voiceVolume; + set => voiceVolume = Math.Clamp(value, 0f, MaxVoiceChatBoost); + } + private SoundChannel radioNoiseChannel; private float radioNoise; @@ -23,7 +34,6 @@ namespace Barotrauma.Networking set { radioNoise = MathHelper.Clamp(value, 0.0f, 1.0f); } } - private bool mutedLocally; public bool MutedLocally { @@ -86,6 +96,17 @@ namespace Barotrauma.Networking float dist = Vector3.Distance(new Vector3(character.WorldPosition, 0.0f), GameMain.SoundManager.ListenerPosition); gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist); } + if (!VoipSound.UsingRadio) + { + //emulate the "garbling" of the text chat + //this in a sense means the volume diminishes exponentially when close to the maximum range of the sound + //(diminished by both the garbling and the distance attenuation) + + //which is good, because we want the voice chat to become unintelligible close to the max range, + //and we need to heavily reduce the volume to do that (otherwise it's just quiet, but still intelligible) + float garbleAmount = ChatMessage.GetGarbleAmount(Character.Controlled, character, ChatMessage.SpeakRangeVOIP); + gain *= 1.0f - garbleAmount; + } if (RadioNoise > 0.0f) { noiseGain = gain * RadioNoise; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 8ce5ecdea..6ba6e36a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -21,7 +21,12 @@ namespace Barotrauma DebugConsole.Log($"Received entity removal message for \"{entity}\"."); if (entity is Item item && item.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); + if (item.Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every deconstructed item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); + } } entity.Remove(); } @@ -45,7 +50,12 @@ namespace Barotrauma { if (newItem.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier); + if (newItem.Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every fabricated item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier); + } } receivedEvents.Add((newItem, false)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 2971f30e8..dbfebfc11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Networking public readonly List ServerSubmarines = new List(); - public string ServerName { get; private set; } + public string ServerName => ServerSettings.ServerName; private bool canStart; @@ -270,11 +270,11 @@ namespace Barotrauma.Networking otherClients = new List(); - ServerSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false, System.Net.IPAddress.Any); + ServerSettings = new ServerSettings(this, serverName, 0, 0, 0, false, false, System.Net.IPAddress.Any); Voting = new Voting(); serverEndpoints = endpoints; - InitiateServerJoin(serverName); + InitiateServerJoin(); //ServerLog = new ServerLog(""); @@ -289,7 +289,7 @@ namespace Barotrauma.Networking return serverInfo; } - private void InitiateServerJoin(string hostName) + private void InitiateServerJoin() { LastClientListUpdateID = 0; @@ -306,8 +306,6 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.ChatInput.Enabled = false; } - ServerName = hostName; - myCharacter = Character.Controlled; ChatMessage.LastID = 0; @@ -318,9 +316,8 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } - public void SetLobbyPublic(bool isPublic) + public static void SetLobbyPublic(bool isPublic) { - GameMain.NetLobbyScreen.SetPublic(isPublic); SteamManager.SetLobbyPublic(isPublic); } @@ -725,7 +722,7 @@ namespace Barotrauma.Networking if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) { - CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound"); + CoroutineManager.StartCoroutine(NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound"); } break; case ServerPacketHeader.STARTGAME: @@ -1098,7 +1095,7 @@ namespace Barotrauma.Networking var prevContentPackages = ClientPeer.ServerContentPackages; //decrement lobby update ID to make sure we update the lobby when we reconnect GameMain.NetLobbyScreen.LastUpdateID--; - InitiateServerJoin(ServerName); + InitiateServerJoin(); if (ClientPeer != null) { //restore the previous list of content packages so we can reconnect immediately without having to recheck that the packages match @@ -1191,7 +1188,7 @@ namespace Barotrauma.Networking { if (!CoroutineManager.IsCoroutineRunning("WaitForStartingInfo")) { - InitiateServerJoin(ServerName); + InitiateServerJoin(); yield return new WaitForSeconds(5.0f); } yield return new WaitForSeconds(0.5f); @@ -1255,7 +1252,7 @@ namespace Barotrauma.Networking if (!(this.permittedConsoleCommands.Any(c => !permittedConsoleCommands.Contains(c)) || permittedConsoleCommands.Any(c => !this.permittedConsoleCommands.Contains(c)))) { - if (newPermissions == permissions) return; + if (newPermissions == permissions) { return; } } bool refreshCampaignUI = permissions.HasFlag(ClientPermissions.ManageCampaign) != newPermissions.HasFlag(ClientPermissions.ManageCampaign) || @@ -1337,6 +1334,8 @@ namespace Barotrauma.Networking } GameMain.NetLobbyScreen.RefreshEnabledElements(); + //close settings menu in case it was open + ServerSettings.Close(); OnPermissionChanged.Invoke(new PermissionChangedEvent(permissions, this.permittedConsoleCommands)); } @@ -2027,18 +2026,15 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.LastUpdateID = updateID; ServerSettings.ServerLog.ServerName = ServerSettings.ServerName; - - if (!GameMain.NetLobbyScreen.ServerName.Selected) { GameMain.NetLobbyScreen.ServerName.Text = ServerSettings.ServerName; } - if (!GameMain.NetLobbyScreen.ServerMessage.Selected) { GameMain.NetLobbyScreen.ServerMessage.Text = ServerSettings.ServerMessageText; } GameMain.NetLobbyScreen.UsingShuttle = usingShuttle; - if (!allowSubVoting) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } + if (!allowSubVoting || GameMain.NetLobbyScreen.SelectedSub == null) { GameMain.NetLobbyScreen.TrySelectSub(selectSubName, selectSubHash, GameMain.NetLobbyScreen.SubList); } GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); GameMain.NetLobbyScreen.SetTraitorDangerLevel(traitorDangerLevel); - GameMain.NetLobbyScreen.SetMissionType(missionType); + GameMain.NetLobbyScreen.LevelSeed = levelSeed; GameMain.NetLobbyScreen.SelectMode(modeIndex); if (isInitialUpdate && GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) @@ -2055,7 +2051,6 @@ namespace Barotrauma.Networking } GameMain.NetLobbyScreen.SetAllowSpectating(allowSpectating); - GameMain.NetLobbyScreen.LevelSeed = levelSeed; GameMain.NetLobbyScreen.SetLevelDifficulty(levelDifficulty); GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode); GameMain.NetLobbyScreen.SetBotCount(botCount); @@ -2576,12 +2571,17 @@ namespace Barotrauma.Networking } public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) + { + CreateEntityEvent(entity, extraData, requireControlledCharacter: true); + } + + public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData, bool requireControlledCharacter) { if (entity is not IClientSerializable clientSerializable) { throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); } - EntityEventManager.CreateEvent(clientSerializable, extraData); + EntityEventManager.CreateEvent(clientSerializable, extraData, requireControlledCharacter); } public bool HasPermission(ClientPermissions permission) @@ -2708,11 +2708,21 @@ namespace Barotrauma.Networking public override void AddChatMessage(ChatMessage message) { var should = GameMain.LuaCs.Hook.Call("chatMessage", message.Text, message.SenderClient, message.Type, message); - if (should != null && should.Value) return; - - base.AddChatMessage(message); + if (should != null && should.Value) { return; } if (string.IsNullOrEmpty(message.Text)) { return; } + if (message.Sender != null && !message.Sender.IsDead) + { + if (message.Text.IsNullOrEmpty()) + { + message.Sender.ShowTextlessSpeechBubble(2.0f, message.Color); + + } + else + { + message.Sender.ShowSpeechBubble(message.Color, message.Text); + } + } GameMain.NetLobbyScreen.NewChatMessage(message); chatBox.AddMessage(message); } @@ -2901,7 +2911,7 @@ namespace Barotrauma.Networking ClientPeer.Send(msg, DeliveryMethod.Reliable); } - public bool SpectateClicked(GUIButton button, object _) + public bool JoinOnGoingClicked(GUIButton button, object _) { MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? @@ -3290,17 +3300,36 @@ namespace Barotrauma.Networking private void CreateSelectionRelatedButtons(Client client, GUIComponent frame) { - var content = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f - frame.RectTransform.RelativeSize.Y), frame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), - style: null); + var content = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1.0f - frame.RectTransform.RelativeSize.Y), frame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), childAnchor: Anchor.TopCenter); - var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.TopCenter), + var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform, Anchor.TopCenter), TextManager.Get("Mute")) { Selected = client.MutedLocally, OnSelected = (tickBox) => { client.MutedLocally = tickBox.Selected; return true; } }; + + var volumeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.45f), content.RectTransform, Anchor.TopCenter), isHorizontal: false); - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), content.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft) + var volumeTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var label = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get("VoiceChatVolume")); + var percentageText = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(client.VoiceVolume), textAlignment: Alignment.Right); + + var volumeSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style: "GUISlider") + { + Range = new Vector2(0f, 1f), + BarScroll = client.VoiceVolume / Client.MaxVoiceChatBoost, + OnMoved = (_, barScroll) => + { + float newVolume = barScroll * Client.MaxVoiceChatBoost; + + client.VoiceVolume = newVolume; + percentageText.Text = ToolBox.GetFormattedPercentage(newVolume); + return true; + } + }; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft) { RelativeSpacing = 0.05f, Stretch = true @@ -3334,7 +3363,7 @@ namespace Barotrauma.Networking TextManager.Get("Ban"), style: "GUIButtonSmall") { UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.BanPlayer(client); return false; } + OnClicked = (btn, userdata) => { NetLobbyScreen.BanPlayer(client); return false; } }; } if (HasPermission(ClientPermissions.Kick) && client.AllowKicking) @@ -3343,7 +3372,7 @@ namespace Barotrauma.Networking TextManager.Get("Kick"), style: "GUIButtonSmall") { UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } + OnClicked = (btn, userdata) => { NetLobbyScreen.KickPlayer(client); return false; } }; } else if (ServerSettings.AllowVoteKick && client.AllowKicking) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 3db7e69c3..c50dc2d86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -42,16 +42,17 @@ namespace Barotrauma.Networking thisClient = client; } - public void CreateEvent(IClientSerializable entity, NetEntityEvent.IData extraData = null) + public void CreateEvent(IClientSerializable entity, NetEntityEvent.IData extraData = null, bool requireControlledCharacter = true) { - if (GameMain.Client?.Character == null) { return; } + if (GameMain.Client == null) { return; } + if (requireControlledCharacter && GameMain.Client.Character == null) { return; } if (!ValidateEntity(entity)) { return; } var newEvent = new ClientEntityEvent( entity, eventId: (UInt16)(ID + 1), - characterStateId: GameMain.Client.Character.LastNetworkUpdateID); + characterStateId: GameMain.Client.Character?.LastNetworkUpdateID ?? Entity.NullEntityID); if (extraData != null) { newEvent.SetData(extraData); } for (int i = events.Count - 1; i >= 0; i--) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index f357f1a2b..5ecc23923 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -18,6 +18,8 @@ namespace Barotrauma.Networking public ImmutableArray ServerContentPackages { get; set; } = ImmutableArray.Empty; + public bool AllowModDownloads { get; private set; } = true; + public readonly record struct Callbacks( Callbacks.MessageCallback OnMessageReceived, Callbacks.DisconnectCallback OnDisconnect, @@ -151,6 +153,7 @@ namespace Barotrauma.Networking if (!ContentPackageOrderReceived) { ServerContentPackages = orderPacket.ContentPackages; + AllowModDownloads = orderPacket.AllowModDownloads; if (ServerContentPackages.Length == 0) { string errorMsg = "Error in ContentPackageOrder message: list of content packages enabled on the server was empty."; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 5441869bd..bd219b5f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -51,20 +51,38 @@ namespace Barotrauma.Networking { if (Character.Controlled != null || (GameMain.GameSession is not { IsRunning: true })) { return; } - LocalizedString text = TextManager.Get("respawnquestionprompt"); + LocalizedString text; + GUIMessageBox respawnPrompt; + if (SkillLossPercentageOnImmediateRespawn > 0) + { + // Respawn asap with extra skill loss? + text = TextManager.GetWithVariable("respawnquestionprompt", "[percentage]", ((int)Math.Round(SkillLossPercentageOnImmediateRespawn)).ToString()); + respawnPrompt = new GUIMessageBox( + TextManager.Get("tutorial.tryagainheader"), text, + new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) + { + UserData = "respawnquestionprompt" + }; + } + else + { + // Respawn asap? + text = TextManager.Get("respawnquestionpromptnoloss"); + respawnPrompt = new GUIMessageBox( + TextManager.Get("tutorial.tryagainheader"), text, + new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawnnoloss"), TextManager.Get("respawnquestionpromptwait") }) + { + UserData = "respawnquestionprompt" + }; + } if (SkillLossPercentageOnDeath > 0) { + // You have died... etc added BEFORE the above text text = TextManager.GetWithVariable("respawnskillpenalty", "[percentage]", ((int)SkillLossPercentageOnDeath).ToString()) + "\n\n" + text; }; - var respawnPrompt = new GUIMessageBox( - TextManager.Get("tutorial.tryagainheader"), text, - new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) - { - UserData = "respawnquestionprompt" - }; respawnPrompt.Buttons[0].OnClicked += (btn, userdata) => { GameMain.Client?.SendRespawnPromptResponse(waitForNextRoundRespawn: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index fa077332e..b7252310c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using Barotrauma.Steam; namespace Barotrauma.Networking { @@ -28,9 +27,9 @@ namespace Barotrauma.Networking { get { - if (GUIComponent == null) return null; - else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected; - else if (GUIComponent is GUITextBox textBox) return textBox.Text; + if (GUIComponent == null) { return null; } + else if (GUIComponent is GUITickBox tickBox) { return tickBox.Selected; } + else if (GUIComponent is GUITextBox textBox) { return textBox.Text; } else if (GUIComponent is GUIScrollBar scrollBar) { if (property.PropertyType == typeof(int)) @@ -40,19 +39,23 @@ namespace Barotrauma.Networking return scrollBar.BarScrollValue; } - else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected; - else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; + else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) { return radioButtonGroup.Selected; } + else if (GUIComponent is GUIDropDown dropdown) { return dropdown.SelectedData; } else if (GUIComponent is GUINumberInput numInput) { if (numInput.InputType == NumberType.Int) { return numInput.IntValue; } else { return numInput.FloatValue; } } + else if (GUIComponent is IGUISelectionCarouselAccessor selectionCarousel) + { + return selectionCarousel.GetSelectedElement(); + } return null; } set { - if (GUIComponent == null) return; - else if (GUIComponent is GUITickBox tickBox) tickBox.Selected = (bool)value; - else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value; + if (GUIComponent == null) { return; } + else if (GUIComponent is GUITickBox tickBox) { tickBox.Selected = (bool)value; } + else if (GUIComponent is GUITextBox textBox) { textBox.Text = (string)value; } else if (GUIComponent is GUIScrollBar scrollBar) { if (value is int i) @@ -63,9 +66,10 @@ namespace Barotrauma.Networking { scrollBar.BarScrollValue = (float)value; } + scrollBar.OnMoved?.Invoke(scrollBar, scrollBar.BarScroll); } - else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) radioButtonGroup.Selected = (int)value; - else if (GUIComponent is GUIDropDown dropdown) dropdown.SelectItem(value); + else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) { radioButtonGroup.Selected = (int)value; } + else if (GUIComponent is GUIDropDown dropdown) { dropdown.SelectItem(value); } else if (GUIComponent is GUINumberInput numInput) { if (numInput.InputType == NumberType.Int) @@ -77,6 +81,10 @@ namespace Barotrauma.Networking numInput.FloatValue = (float)value; } } + else if (GUIComponent is IGUISelectionCarouselAccessor selectionCarousel) + { + selectionCarousel.SelectElement(value); + } } } @@ -84,7 +92,7 @@ namespace Barotrauma.Networking { get { - if (GUIComponent == null) return false; + if (GUIComponent == null) { return false; } return !PropEquals(TempValue, GUIComponentValue); } } @@ -131,35 +139,45 @@ namespace Barotrauma.Networking } } - ReadMonsterEnabled(incMsg); + if (ReadMonsterEnabled(incMsg)) + { + if (monstersEnabledPanel is { Visible: true }) + { + //refresh panel if someone else changes it + monstersEnabledPanel.Parent?.RemoveChild(monstersEnabledPanel); + monstersEnabledPanel = CreateMonstersEnabledPanel(); + monstersEnabledPanel.Visible = true; + } + } BanList.ClientAdminRead(incMsg); + GameMain.NetLobbyScreen?.RefreshPlaystyleIcons(); } public void ClientRead(IReadMessage incMsg) { NetFlags requiredFlags = (NetFlags)incMsg.ReadByte(); - if (requiredFlags.HasFlag(NetFlags.Name)) - { - ServerName = incMsg.ReadString(); - } - - if (requiredFlags.HasFlag(NetFlags.Message)) - { - ServerMessageText = incMsg.ReadString(); - } PlayStyle = (PlayStyle)incMsg.ReadByte(); MaxPlayers = incMsg.ReadByte(); HasPassword = incMsg.ReadBoolean(); IsPublic = incMsg.ReadBoolean(); - GameMain.Client?.SetLobbyPublic(IsPublic); + GameClient.SetLobbyPublic(IsPublic); AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); if (requiredFlags.HasFlag(NetFlags.Properties)) { - ReadExtraCargo(incMsg); + if (ReadExtraCargo(incMsg)) + { + if (extraCargoPanel is { Visible: true }) + { + //refresh panel if someone else changes it + extraCargoPanel.Parent?.RemoveChild(extraCargoPanel); + extraCargoPanel = CreateExtraCargoPanel(); + extraCargoPanel.Visible = true; + } + } } if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) @@ -180,13 +198,7 @@ namespace Barotrauma.Networking NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, - float? levelDifficulty = null, - bool? autoRestart = null, - float? traitorProbability = null, - int traitorDangerLevel = 0, - int botCount = 0, - int botSpawnMode = 0, - bool? useRespawnShuttle = null) + int traitorDangerLevel = 0) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; } @@ -196,24 +208,6 @@ namespace Barotrauma.Networking outMsg.WriteByte((byte)dataToSend); - if (dataToSend.HasFlag(NetFlags.Name)) - { - if (GameMain.NetLobbyScreen.ServerName.Text != ServerName) - { - ServerName = GameMain.NetLobbyScreen.ServerName.Text; - } - outMsg.WriteString(ServerName); - } - - if (dataToSend.HasFlag(NetFlags.Message)) - { - if (GameMain.NetLobbyScreen.ServerMessage.Text != ServerMessageText) - { - ServerMessageText = GameMain.NetLobbyScreen.ServerMessage.Text; - } - outMsg.WriteString(ServerMessageText); - } - if (dataToSend.HasFlag(NetFlags.Properties)) { //TODO: split this up? @@ -245,966 +239,29 @@ namespace Barotrauma.Networking { outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); outMsg.WriteRangedInteger(missionTypeAnd ?? (int)Barotrauma.MissionType.All, 0, (int)Barotrauma.MissionType.All); - - outMsg.WriteBoolean(traitorProbability != null); - outMsg.WriteSingle(traitorProbability ?? 0.0f); outMsg.WriteByte((byte)(traitorDangerLevel + 1)); - - outMsg.WriteByte((byte)(botCount + 1)); - outMsg.WriteByte((byte)(botSpawnMode + 1)); - - outMsg.WriteSingle(levelDifficulty ?? -1000.0f); - - outMsg.WriteBoolean(useRespawnShuttle != null); - outMsg.WriteBoolean(useRespawnShuttle ?? false); - - outMsg.WriteBoolean(autoRestart != null); - outMsg.WriteBoolean(autoRestart ?? false); - outMsg.WritePadBits(); } if (dataToSend.HasFlag(NetFlags.LevelSeed)) { - outMsg.WriteString(GameMain.NetLobbyScreen.SeedBox.Text); + outMsg.WriteString(GameMain.NetLobbyScreen.LevelSeedBox.Text); } GameMain.Client.ClientPeer.Send(outMsg, DeliveryMethod.Reliable); } - //GUI stuff - private GUIFrame settingsFrame; - private GUIFrame[] settingsTabs; - private GUIButton[] tabButtons; - private int settingsTabIndex; - - private GUIDropDown karmaPresetDD; - private GUIComponent karmaSettingsBlocker; - - enum SettingsTab - { - General, - Rounds, - Antigriefing, - Banlist - } - private NetPropertyData GetPropertyData(string name) { - return netProperties.First(p => p.Value.Name == name).Value; - } - - public void AssignGUIComponent(string propertyName, GUIComponent component) - { - GetPropertyData(propertyName).AssignGUIComponent(component); - } - - public void AddToGUIUpdateList() - { - if (GUI.DisableHUD) return; - - settingsFrame?.AddToGUIUpdateList(); - } - - private void CreateSettingsFrame() - { - foreach (NetPropertyData prop in netProperties.Values) + var matchingProperty = netProperties.FirstOrDefault(p => p.Value.Name == name); + if (matchingProperty.Equals(default(KeyValuePair))) { - prop.TempValue = prop.Value; - } - - //background frame - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - - new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => - { - if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ToggleSettingsFrame(btn, userData); } - return true; - }; - - new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null) - { - OnClicked = ToggleSettingsFrame - }; - - //center frames - GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.85f), settingsFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 430) }); - GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), innerFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("Settings"), font: GUIStyle.LargeFont); - - var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedFrame.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - var tabContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform), style: "InnerFrame"); - - //tabs - LocalizedString[] tabNames = - Enum.GetValues(typeof(SettingsTab)).Cast() - .Select(tv => TextManager.Get("ServerSettings" + tv + "Tab")).ToArray(); - settingsTabs = new GUIFrame[tabNames.Length]; - tabButtons = new GUIButton[tabNames.Length]; - for (int i = 0; i < tabNames.Length; i++) - { - settingsTabs[i] = new GUIFrame(new RectTransform(Vector2.One, tabContent.RectTransform, Anchor.Center), style: null); - tabButtons[i] = new GUIButton(new RectTransform(new Vector2(0.2f, 1.2f), buttonArea.RectTransform), tabNames[i], style: "GUITabButton") - { - UserData = i, - OnClicked = SelectSettingsTab - }; - } - GUITextBlock.AutoScaleAndNormalize(tabButtons.Select(b => b.TextBlock)); - SelectSettingsTab(tabButtons[0], 0); - - //"Close" - var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.05f), paddedFrame.RectTransform), style: null); - var closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) - { - OnClicked = ToggleSettingsFrame - }; - - - //-------------------------------------------------------------------------------- - // server settings - //-------------------------------------------------------------------------------- - - var serverTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.General].RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - //*********************************************** - - // Language - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont); - var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform)); - foreach (var language in ServerLanguageOptions.Options) - { - languageDD.AddItem(language.Label, language.Identifier); - } - GetPropertyData(nameof(Language)).AssignGUIComponent(languageDD); - - //changing server visibility on the fly is not supported in dedicated servers - if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) - { - var isPublic = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), - TextManager.Get("publicserver")) - { - ToolTip = TextManager.Get("publicservertooltip") - }; - GetPropertyData(nameof(IsPublic)).AssignGUIComponent(isPublic); - } - - // Sub Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUIStyle.SubHeadingFont); - var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - GUIRadioButtonGroup selectionMode = new GUIRadioButtonGroup(); - for (int i = 0; i < 3; i++) - { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); - selectionMode.AddRadioButton(i, selectionTick); - } - selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); - selectionFrame.RectTransform.IsFixedSize = true; - - GetPropertyData(nameof(SubSelectionMode)).AssignGUIComponent(selectionMode); - - // Mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection"), font: GUIStyle.SubHeadingFont); - selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - selectionMode = new GUIRadioButtonGroup(); - for (int i = 0; i < 3; i++) - { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); - selectionMode.AddRadioButton(i, selectionTick); - } - selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); - selectionFrame.RectTransform.IsFixedSize = true; - GetPropertyData(nameof(ModeSelectionMode)).AssignGUIComponent(selectionMode); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), style: "HorizontalLine"); - - //*********************************************** - - var voiceChatEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), - TextManager.Get("ServerSettingsVoiceChatEnabled")); - GetPropertyData(nameof(VoiceChatEnabled)).AssignGUIComponent(voiceChatEnabled); - - //*********************************************** - - LocalizedString autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; - var startIntervalText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), autoRestartDelayLabel); - var startIntervalSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), barSize: 0.1f, style: "GUISlider") - { - UserData = startIntervalText, - Step = 0.05f, - OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - GUITextBlock text = scrollBar.UserData as GUITextBlock; - text.Text = autoRestartDelayLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - return true; - } - }; - startIntervalSlider.Range = new Vector2(10.0f, 300.0f); - GetPropertyData(nameof(AutoRestartInterval)).AssignGUIComponent(startIntervalSlider); - startIntervalSlider.OnMoved(startIntervalSlider, startIntervalSlider.BarScroll); - - //*********************************************** - - var startWhenClientsReady = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), - TextManager.Get("ServerSettingsStartWhenClientsReady")); - GetPropertyData(nameof(StartWhenClientsReady)).AssignGUIComponent(startWhenClientsReady); - - CreateLabeledSlider(serverTab, "ServerSettingsStartWhenClientsReadyRatio", out GUIScrollBar slider, out GUITextBlock sliderLabel); - LocalizedString clientsReadyRequiredLabel = sliderLabel.Text; - slider.Step = 0.2f; - slider.Range = new Vector2(0.5f, 1.0f); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = clientsReadyRequiredLabel.Replace("[percentage]", ((int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f)).ToString()); - return true; - }; - GetPropertyData(nameof(StartWhenClientsReadyRatio)).AssignGUIComponent(slider); - slider.OnMoved(slider, slider.BarScroll); - - //*********************************************** - - var allowSpecBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsAllowSpectating")); - GetPropertyData(nameof(AllowSpectating)).AssignGUIComponent(allowSpecBox); - - var shareSubsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsShareSubFiles")); - GetPropertyData(nameof(AllowFileTransfers)).AssignGUIComponent(shareSubsBox); - - var randomizeLevelBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsRandomizeSeed")); - GetPropertyData(nameof(RandomizeSeed)).AssignGUIComponent(randomizeLevelBox); - - var saveLogsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsSaveLogs")) - { - OnSelected = (GUITickBox) => - { - //TODO: fix? - //showLogButton.Visible = SaveServerLogs; - return true; - } - }; - GetPropertyData(nameof(SaveServerLogs)).AssignGUIComponent(saveLogsBox); - - //-------------------------------------------------------------------------------- - // game settings - //-------------------------------------------------------------------------------- - - var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)); - var roundsContent = new GUIListBox(new RectTransform(Vector2.One, roundsTab.RectTransform, Anchor.Center), style: "GUIListBoxNoBorder").Content; - - GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsContent.RectTransform)); - // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), playStyleLayout.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); - var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), playStyleLayout.RectTransform)) - { - AutoHideScrollBar = true, - UseGridLayout = true - }; - playstyleList.Padding *= 2.0f; - - List playStyleTickBoxes = new List(); - GUIRadioButtonGroup selectionPlayStyle = new GUIRadioButtonGroup(); - foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) - { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.32f, 0.49f), playstyleList.Content.RectTransform), TextManager.Get("servertag." + playStyle), font: GUIStyle.SmallFont, style: "GUIRadioButton") - { - ToolTip = TextManager.Get("servertagdescription." + playStyle) - }; - selectionPlayStyle.AddRadioButton((int)playStyle, selectionTick); - playStyleTickBoxes.Add(selectionTick); - } - GetPropertyData(nameof(PlayStyle)).AssignGUIComponent(selectionPlayStyle); - GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); - playstyleList.RectTransform.MinSize = new Point(0, (int)(playstyleList.Content.Children.First().Rect.Height * 2.0f + playstyleList.Padding.Y + playstyleList.Padding.W)); - - GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsContent.RectTransform)) - { - Stretch = true - }; - - var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), - TextManager.Get("ServerSettingsEndRoundVoting")); - GetPropertyData(nameof(AllowEndVoting)).AssignGUIComponent(endVoteBox); - - CreateLabeledSlider(sliderLayout, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); - - LocalizedString endRoundLabel = sliderLabel.Text; - slider.Step = 0.2f; - slider.Range = new Vector2(0.5f, 1.0f); - GetPropertyData(nameof(EndVoteRequiredRatio)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = endRoundLabel + " " + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; - return true; - }; - slider.OnMoved(slider, slider.BarScroll); - - LocalizedString skillLossLabel = TextManager.Get("ServerSettingsSkillLossPercentageOnDeath"); - var skillLossText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), roundsContent.RectTransform), skillLossLabel); - var skillLossSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), roundsContent.RectTransform), barSize: 0.1f, style: "GUISlider") - { - UserData = skillLossText, - Range = new Vector2(0, 100), - StepValue = 1, - OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - GUITextBlock text = scrollBar.UserData as GUITextBlock; - text.Text = TextManager.AddPunctuation( - ':', - skillLossLabel, - TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString())); - return true; - } - }; - GetPropertyData(nameof(SkillLossPercentageOnDeath)).AssignGUIComponent(skillLossSlider); - skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); - - var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), sliderLayout.RectTransform), - TextManager.Get("ServerSettingsAllowRespawning")); - GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); - - CreateLabeledSlider(sliderLayout, "ServerSettingsRespawnInterval", out slider, out sliderLabel); - LocalizedString intervalLabel = sliderLabel.Text; - slider.Range = new Vector2(10.0f, 600.0f); - slider.StepValue = 10.0f; - GetPropertyData(nameof(RespawnInterval)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - GUITextBlock text = scrollBar.UserData as GUITextBlock; - text.Text = intervalLabel + " " + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - return true; - }; - slider.OnMoved(slider, slider.BarScroll); - - var respawnLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), sliderLayout.RectTransform), - isHorizontal: true); - - var minRespawnLayout - = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnLayout.RectTransform)); - - var minRespawnText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), minRespawnLayout.RectTransform), "") - { - ToolTip = TextManager.Get("ServerSettingsMinRespawnToolTip") - }; - - LocalizedString minRespawnLabel = TextManager.Get("ServerSettingsMinRespawn") + " "; - CreateLabeledSlider(minRespawnLayout, "", out slider, out sliderLabel); - sliderLabel.RectTransform.RelativeSize = Vector2.Zero; - slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = minRespawnText.ToolTip; - slider.UserData = minRespawnText; - slider.Step = 0.1f; - slider.Range = new Vector2(0.0f, 1.0f); - GetPropertyData(nameof(MinRespawnRatio)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = minRespawnLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; - return true; - }; - slider.OnMoved(slider, MinRespawnRatio); - - var respawnDurationLayout - = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), respawnLayout.RectTransform)); - - var respawnDurationText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), respawnDurationLayout.RectTransform), "") - { - ToolTip = TextManager.Get("ServerSettingsRespawnDurationToolTip") - }; - - LocalizedString respawnDurationLabel = TextManager.Get("ServerSettingsRespawnDuration") + " "; - CreateLabeledSlider(respawnDurationLayout, "", out slider, out sliderLabel); - sliderLabel.RectTransform.RelativeSize = Vector2.Zero; - slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = respawnDurationText.ToolTip; - slider.UserData = respawnDurationText; - slider.Step = 0.1f; - slider.Range = new Vector2(60.0f, 660.0f); - slider.ScrollToValue = (GUIScrollBar scrollBar, float barScroll) => - { - return barScroll >= 1.0f ? 0.0f : barScroll * (scrollBar.Range.Y - scrollBar.Range.X) + scrollBar.Range.X; - }; - slider.ValueToScroll = (GUIScrollBar scrollBar, float value) => - { - return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X); - }; - GetPropertyData(nameof(MaxTransportTime)).AssignGUIComponent(slider); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - if (barScroll == 1.0f) - { - ((GUITextBlock)scrollBar.UserData).Text = respawnDurationLabel + TextManager.Get("Unlimited"); - } - else - { - ((GUITextBlock)scrollBar.UserData).Text = respawnDurationLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - } - - return true; - }; - slider.OnMoved(slider, slider.BarScroll); - - GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); - - var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), losModeLayout.RectTransform), - TextManager.Get("LosEffect")); - - var losModeRadioButtonLayout - = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), losModeLayout.RectTransform), - isHorizontal: true) - { - Stretch = true - }; - - var losModeRadioButtonGroup = new GUIRadioButtonGroup(); - LosMode[] losModes = (LosMode[])Enum.GetValues(typeof(LosMode)); - for (int i = 0; i < losModes.Length; i++) - { - var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), losModeRadioButtonLayout.RectTransform), TextManager.Get($"LosMode{losModes[i]}"), font: GUIStyle.SmallFont, style: "GUIRadioButton"); - losModeRadioButtonGroup.AddRadioButton(i, losTick); - } - GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); - - GUILayoutGroup healthBarModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); - - var healthBarModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), healthBarModeLayout.RectTransform), - TextManager.Get("ShowEnemyHealthBars")); - - var healthBarModeRadioButtonLayout - = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), healthBarModeLayout.RectTransform), - isHorizontal: true) - { - Stretch = true - }; - - var healthBarModeRadioButtonGroup = new GUIRadioButtonGroup(); - EnemyHealthBarMode[] healthBarModeModes = Enum.GetValues(); - for (int i = 0; i < healthBarModeModes.Length; i++) - { - var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), healthBarModeRadioButtonLayout.RectTransform), - TextManager.Get($"ShowEnemyHealthBars.{healthBarModeModes[i]}"), - font: GUIStyle.SmallFont, style: "GUIRadioButton"); - healthBarModeRadioButtonGroup.AddRadioButton(i, losTick); - } - GetPropertyData(nameof(ShowEnemyHealthBars)).AssignGUIComponent(healthBarModeRadioButtonGroup); - - GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsContent.RectTransform)) - { - Stretch = true - }; - - var traitorsMinPlayerCount = CreateLabeledNumberInput(numberLayout, "ServerSettingsTraitorsMinPlayerCount", 2, 16, "ServerSettingsTraitorsMinPlayerCountToolTip"); - GetPropertyData(nameof(TraitorsMinPlayerCount)).AssignGUIComponent(traitorsMinPlayerCount); - - var maximumTransferAmount = CreateLabeledNumberInput(numberLayout, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); - GetPropertyData(nameof(MaximumMoneyTransferRequest)).AssignGUIComponent(maximumTransferAmount); - - var lootedMoneyDestination = CreateLabeledDropdown(numberLayout, "serversettingslootedmoneydestination", numElements: 2, "serversettingslootedmoneydestinationtooltip"); - lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.bank"), LootedMoneyDestination.Bank); - lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.wallet"), LootedMoneyDestination.Wallet); - GetPropertyData(nameof(LootedMoneyDestination)).AssignGUIComponent(lootedMoneyDestination); - - var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); - GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); - - GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsContent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - var monsterButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), - TextManager.Get("ServerSettingsMonsterSpawns"), style: "GUIButtonSmall") - { - Enabled = !GameMain.NetworkMember.GameStarted - }; - var monsterFrame = new GUIListBox(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomLeft, Pivot.BottomRight)) - { - Visible = false - }; - monsterButton.UserData = monsterFrame; - monsterButton.OnClicked = (button, obj) => - { - if (GameMain.NetworkMember.GameStarted) - { - ((GUIComponent)obj).Visible = false; - button.Enabled = false; - return true; - } - ((GUIComponent)obj).Visible = !((GUIComponent)obj).Visible; - return true; - }; - - InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys.ToList(); - tempMonsterEnabled = new Dictionary(MonsterEnabled); - foreach (Identifier s in monsterNames) - { - LocalizedString translatedLabel = TextManager.Get($"Character.{s}").Fallback(s.Value); - var monsterEnabledBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), monsterFrame.Content.RectTransform) { MinSize = new Point(0, 25) }, - label: translatedLabel) - { - Selected = tempMonsterEnabled[s], - OnSelected = (GUITickBox tb) => - { - tempMonsterEnabled[s] = tb.Selected; - return true; - } - }; - } - - var cargoButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), - TextManager.Get("ServerSettingsAdditionalCargo"), style: "GUIButtonSmall") - { - Enabled = !GameMain.NetworkMember.GameStarted - }; - - var cargoFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomRight, Pivot.BottomLeft)) - { - 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.OnClicked = (button, obj) => - { - if (GameMain.NetworkMember.GameStarted) - { - ((GUIComponent)obj).Visible = false; - button.Enabled = false; - return true; - } - ((GUIComponent)obj).Visible = !((GUIComponent)obj).Visible; - return true; - }; - - GUITextBlock.AutoScaleAndNormalize(buttonHolder.Children.Select(c => ((GUIButton)c).TextBlock)); - - foreach (ItemPrefab ip in ItemPrefab.Prefabs.OrderBy(ip => ip.Name)) - { - if (ip.AllowAsExtraCargo.HasValue) - { - if (!ip.AllowAsExtraCargo.Value) { continue; } - } - else - { - if (!ip.CanBeBought) { continue; } - } - - var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoList.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true) - { - Stretch = true, - UserData = ip, - RelativeSpacing = 0.05f - }; - - if (ip.InventoryIcon != null || ip.Sprite != null) - { - GUIImage img = new GUIImage(new RectTransform(new Point(itemFrame.Rect.Height), itemFrame.RectTransform), - ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) - { - CanBeFocused = false - }; - img.Color = img.Sprite == ip.InventoryIcon ? ip.InventoryIconColor : ip.SpriteColor; - } - - new GUITextBlock(new RectTransform(new Vector2(0.75f, 1.0f), itemFrame.RectTransform), - ip.Name, font: GUIStyle.SmallFont) - { - Wrap = true, - CanBeFocused = false - }; - - ExtraCargo.TryGetValue(ip, out int cargoVal); - var amountInput = new GUINumberInput(new RectTransform(new Vector2(0.35f, 1.0f), itemFrame.RectTransform), - NumberType.Int, textAlignment: Alignment.CenterLeft) - { - MinValueInt = 0, - MaxValueInt = MaxExtraCargoItemsOfType, - IntValue = cargoVal - }; - amountInput.OnValueChanged += (numberInput) => - { - if (ExtraCargo.ContainsKey(ip)) - { - ExtraCargo[ip] = numberInput.IntValue; - if (numberInput.IntValue <= 0) { ExtraCargo.Remove(ip); } - } - else if (ExtraCargo.Keys.Count() < MaxExtraCargoItemTypes) - { - ExtraCargo.Add(ip, numberInput.IntValue); - } - numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0; - CoroutineManager.Invoke(() => - { - foreach (var child in cargoList.Content.GetAllChildren()) - { - if (child.GetChild() is GUINumberInput otherNumberInput) - { - otherNumberInput.PlusButton.Enabled = ExtraCargo.Keys.Count() < MaxExtraCargoItemTypes && otherNumberInput.IntValue < otherNumberInput.MaxValueInt; - } - } - }, 0.0f); - }; - } - - //-------------------------------------------------------------------------------- - // antigriefing - //-------------------------------------------------------------------------------- - - var antigriefingTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Antigriefing].RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.16f), antigriefingTab.RectTransform)) - { - AutoHideScrollBar = true, - UseGridLayout = true - }; - tickBoxContainer.Padding *= 2.0f; - - var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowFriendlyFire")); - GetPropertyData(nameof(AllowFriendlyFire)).AssignGUIComponent(allowFriendlyFire); - - var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsKillableNPCs")); - GetPropertyData(nameof(KillableNPCs)).AssignGUIComponent(killableNPCs); - - var destructibleOutposts = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsDestructibleOutposts")); - GetPropertyData(nameof(DestructibleOutposts)).AssignGUIComponent(destructibleOutposts); - - var lockAllDefaultWires = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsLockAllDefaultWires")); - GetPropertyData(nameof(LockAllDefaultWires)).AssignGUIComponent(lockAllDefaultWires); - - var allowRewiring = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowRewiring")); - GetPropertyData(nameof(AllowRewiring)).AssignGUIComponent(allowRewiring); - - var allowWifiChatter = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowWifiChat")); - GetPropertyData(nameof(AllowLinkingWifiToChat)).AssignGUIComponent(allowWifiChatter); - - var allowDisguises = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowDisguises")); - GetPropertyData(nameof(AllowDisguises)).AssignGUIComponent(allowDisguises); - - var voteKickBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsAllowVoteKick")); - GetPropertyData(nameof(AllowVoteKick)).AssignGUIComponent(voteKickBox); - - var allowImmediateItemDeliveryBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), - TextManager.Get("ServerSettingsImmediateItemDelivery")); - GetPropertyData(nameof(AllowImmediateItemDelivery)).AssignGUIComponent(allowImmediateItemDeliveryBox); - - GUITextBlock.AutoScaleAndNormalize(tickBoxContainer.Content.Children.Select(c => ((GUITickBox)c).TextBlock)); - - tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); - - CreateLabeledSlider(antigriefingTab, "ServerSettingsKickVotesRequired", out slider, out sliderLabel); - LocalizedString votesRequiredLabel = sliderLabel.Text + " "; - slider.Step = 0.2f; - slider.Range = new Vector2(0.5f, 1.0f); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = votesRequiredLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; - return true; - }; - GetPropertyData(nameof(KickVoteRequiredRatio)).AssignGUIComponent(slider); - slider.OnMoved(slider, slider.BarScroll); - - CreateLabeledSlider(antigriefingTab, "ServerSettingsAutobanTime", out slider, out sliderLabel); - LocalizedString autobanLabel = sliderLabel.Text + " "; - slider.Step = 0.01f; - slider.Range = new Vector2(0.0f, MaxAutoBanTime); - slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - ((GUITextBlock)scrollBar.UserData).Text = autobanLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); - return true; - }; - GetPropertyData(nameof(AutoBanTime)).AssignGUIComponent(slider); - slider.OnMoved(slider, slider.BarScroll); - - var wrongPasswordBanBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsBanAfterWrongPassword")); - GetPropertyData(nameof(BanAfterWrongPassword)).AssignGUIComponent(wrongPasswordBanBox); - var allowedPasswordRetries = CreateLabeledNumberInput(antigriefingTab, "ServerSettingsPasswordRetriesBeforeBan", 0, 10); - GetPropertyData(nameof(MaxPasswordRetriesBeforeBan)).AssignGUIComponent(allowedPasswordRetries); - wrongPasswordBanBox.OnSelected += (tb) => - { - allowedPasswordRetries.Enabled = tb.Selected; - return true; - }; - - - GUILayoutGroup karmaAndDosLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), antigriefingTab.RectTransform), isHorizontal: false); - GUILayoutGroup lowerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); - GUILayoutGroup upperLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true); - - // karma -------------------------------------------------------------------------- - - var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsUseKarma")); - GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox); - - var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection")) - { - ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip") - }; - GetPropertyData(nameof(EnableDoSProtection)).AssignGUIComponent(enableDosProtection); - - CreateLabeledSlider(lowerLayout, "ServerSettingsMaxPacketAmount", out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel); - LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text; - maxPacketSlider.Step = 0.001f; - maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax); - maxPacketSlider.ToolTip = packetAmountTooltip; - maxPacketSlider.OnMoved = (scrollBar, _) => - { - GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData; - int value = (int)MathF.Floor(scrollBar.BarScrollValue); - - LocalizedString valueText = value > PacketLimitMin - ? value.ToString() - : TextManager.Get("ServerSettingsNoLimit"); - - switch (value) - { - case <= PacketLimitMin: - textBlock.TextColor = GUIStyle.Green; - scrollBar.ToolTip = packetAmountTooltip; - break; - case < PacketLimitWarning: - textBlock.TextColor = GUIStyle.Red; - scrollBar.ToolTip = packetAmountTooltipWarning; - break; - default: - textBlock.TextColor = GUIStyle.TextColorNormal; - scrollBar.ToolTip = packetAmountTooltip; - break; - } - - textBlock.Text = $"{maxPacketCountLabel} {valueText}"; - return true; - }; - GetPropertyData(nameof(MaxPacketAmount)).AssignGUIComponent(maxPacketSlider); - maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll); - - karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform)); - foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) - { - karmaPresetDD.AddItem(TextManager.Get("KarmaPreset." + karmaPreset), karmaPreset); - } - - var karmaSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), antigriefingTab.RectTransform), style: null); - var karmaSettingsList = new GUIListBox(new RectTransform(Vector2.One, karmaSettingsContainer.RectTransform)) - { - Spacing = (int)(8 * GUI.Scale) - }; - karmaSettingsList.Padding *= 2.0f; - - karmaSettingsBlocker = new GUIFrame(new RectTransform(Vector2.One, karmaSettingsContainer.RectTransform, Anchor.CenterLeft) - { MaxSize = new Point(karmaSettingsList.ContentBackground.Rect.Width, int.MaxValue) }, style: null) - { - UserData = "karmasettingsblocker", - Color = Color.Black * 0.95f - }; - karmaSettingsBlocker.Color *= 0.5f; - karmaPresetDD.SelectItem(KarmaPreset); - karmaSettingsBlocker.Visible = !karmaBox.Selected || KarmaPreset != "custom"; - GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); - karmaPresetDD.OnSelected = (selected, obj) => - { - string newKarmaPreset = obj as string; - if (newKarmaPreset == KarmaPreset) { return true; } - - List properties = netProperties.Values.ToList(); - List prevValues = new List(); - foreach (NetPropertyData prop in netProperties.Values) - { - prevValues.Add(prop.TempValue); - if (prop.GUIComponent != null) { prop.Value = prop.GUIComponentValue; } - } - if (KarmaPreset == "custom") - { - GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); - GameMain.NetworkMember?.KarmaManager?.Save(); - } - KarmaPreset = newKarmaPreset; - GameMain.NetworkMember.KarmaManager.SelectPreset(KarmaPreset); - karmaSettingsList.Content.ClearChildren(); - karmaSettingsBlocker.Visible = !karmaBox.Selected || KarmaPreset != "custom"; - GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); - for (int i = 0; i < netProperties.Count; i++) - { - properties[i].TempValue = prevValues[i]; - } - return true; - }; - AssignGUIComponent("KarmaPreset", karmaPresetDD); - karmaBox.OnSelected = (tb) => - { - karmaSettingsBlocker.Visible = !tb.Selected || KarmaPreset != "custom"; - return true; - }; - - //-------------------------------------------------------------------------------- - // banlist - //-------------------------------------------------------------------------------- - - BanList.CreateBanFrame(settingsTabs[(int)SettingsTab.Banlist]); - } - - private void CreateLabeledSlider(GUIComponent parent, string labelTag, out GUIScrollBar slider, out GUITextBlock label) - { - var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider"); - label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), - string.IsNullOrEmpty(labelTag) ? "" : TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); - - container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y); - container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y); - - //slider has a reference to the label to change the text when it's used - slider.UserData = label; - } - - private GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag = null) - { - var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f, - ToolTip = TextManager.Get(labelTag) - }; - - var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) - { - AutoScaleHorizontal = true - }; - if (!string.IsNullOrEmpty(toolTipTag)) - { - label.ToolTip = TextManager.Get(toolTipTag); - } - var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) - { - MinValueInt = min, - MaxValueInt = max - }; - - container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); - container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); - - return input; - } - - private GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag = null) - { - var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f, - ToolTip = TextManager.Get(labelTag) - }; - - var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) - { - AutoScaleHorizontal = true - }; - if (!string.IsNullOrEmpty(toolTipTag)) - { - label.ToolTip = TextManager.Get(toolTipTag); - } - var input = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), elementCount: numElements); - - container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); - container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); - - return input; - } - - private bool SelectSettingsTab(GUIButton button, object obj) - { - settingsTabIndex = (int)obj; - - for (int i = 0; i < settingsTabs.Length; i++) - { - settingsTabs[i].Visible = i == settingsTabIndex; - tabButtons[i].Selected = i == settingsTabIndex; - } - - return true; - } - - public bool ToggleSettingsFrame(GUIButton button, object obj) - { - if (GameMain.NetworkMember == null) { return false; } - if (settingsFrame == null) - { - CreateSettingsFrame(); + throw new ArgumentException($"Could not find a {nameof(ServerSettings)} property with the name \"{name}\"."); } else { - if (KarmaPreset == "custom") - { - GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); - GameMain.NetworkMember?.KarmaManager?.Save(); - } - ClientAdminWrite(NetFlags.Properties); - foreach (NetPropertyData prop in netProperties.Values) - { - prop.GUIComponent = null; - } - settingsFrame = null; + return matchingProperty.Value; } - return false; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs new file mode 100644 index 000000000..0aad3ebfa --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettingsUI.cs @@ -0,0 +1,924 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.Networking +{ + partial class ServerSettings : ISerializableEntity + { + //GUI stuff + private GUIFrame settingsFrame; + private readonly Dictionary settingsTabs = new Dictionary(); + private readonly Dictionary tabButtons = new Dictionary(); + private SettingsTab selectedTab; + + //UI elements relating to karma, hidden when karma is disabled + private readonly List karmaElements = new List(); + private GUIDropDown karmaPresetDD; + private GUIListBox karmaSettingsList; + + private GUIComponent extraCargoPanel, monstersEnabledPanel; + private GUIButton extraCargoButton, monstersEnabledButton; + + enum SettingsTab + { + ServerIdentity, + General, + Antigriefing, + Banlist + } + + public void AssignGUIComponent(string propertyName, GUIComponent component) + { + GetPropertyData(propertyName).AssignGUIComponent(component); + } + + public void AddToGUIUpdateList() + { + if (GUI.DisableHUD) { return; } + + settingsFrame?.AddToGUIUpdateList(); + } + + private void CreateSettingsFrame() + { + foreach (NetPropertyData prop in netProperties.Values) + { + prop.TempValue = prop.Value; + } + + //background frame + settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => + { + if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ToggleSettingsFrame(btn, userData); } + return true; + }; + + new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null) + { + OnClicked = ToggleSettingsFrame + }; + + //center frames + GUIFrame innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.85f), settingsFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 430) }); + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(innerFrame.Rect.Size - new Point(GUI.IntScale(20)), innerFrame.RectTransform, Anchor.Center), + childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("serversettingsbutton"), font: GUIStyle.LargeFont); + + var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedFrame.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + var tabContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform), style: "InnerFrame"); + + //tabs + var settingsTabTypes = Enum.GetValues(typeof(SettingsTab)).Cast(); + foreach (var settingsTab in settingsTabTypes) + { + settingsTabs[settingsTab] = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), tabContent.RectTransform, Anchor.Center)); + tabButtons[settingsTab] = new GUIButton(new RectTransform(new Vector2(0.2f, 1.2f), buttonArea.RectTransform), TextManager.Get($"ServerSettings{settingsTab}Tab"), style: "GUITabButton") + { + UserData = settingsTab, + OnClicked = SelectSettingsTab + }; + } + GUITextBlock.AutoScaleAndNormalize(tabButtons.Values.Select(b => b.TextBlock)); + SelectSettingsTab(tabButtons[0], 0); + tabButtons[SettingsTab.Banlist].Enabled = + GameMain.Client.HasPermission(Networking.ClientPermissions.Ban) || + GameMain.Client.HasPermission(Networking.ClientPermissions.Unban); + + //"Close" + var buttonContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.05f), paddedFrame.RectTransform), style: null); + var closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) + { + OnClicked = ToggleSettingsFrame + }; + + CreateServerIdentityTab(settingsTabs[SettingsTab.ServerIdentity]); + CreateGeneralTab(settingsTabs[SettingsTab.General]); + CreateAntigriefingTab(settingsTabs[SettingsTab.Antigriefing]); + CreateBanlistTab(settingsTabs[SettingsTab.Banlist]); + + if (GameMain.Client == null || + !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) + { + //block all settings if the client doesn't have permission to edit them + foreach (var settingsTab in settingsTabs) + { + SetElementInteractability(settingsTab.Value, false); + } + } + //keep these enabled, so clients can open the panels and see what's enabled even if they can't edit them + extraCargoButton.Enabled = monstersEnabledButton.Enabled = true; + } + + private void SetElementInteractability(GUIComponent parent, bool interactable) + { + foreach (var child in parent.GetAllChildren()) + { + child.Enabled = interactable; + //make the disabled color slightly less dim (these should be readable, despite being non-interactable) + child.DisabledColor = new Color(child.Color, child.Color.A / 255.0f * 0.8f); + if (child is GUITextBlock textBlock) + { + textBlock.DisabledTextColor = new Color(textBlock.TextColor, textBlock.TextColor.A / 255.0f * 0.8f); + } + } + } + + private void CreateServerIdentityTab(GUIComponent parent) + { + //changing server visibility on the fly is not supported in dedicated servers + if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) + { + var isPublic = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), + TextManager.Get("publicserver")) + { + ToolTip = TextManager.Get("publicservertooltip") + }; + AssignGUIComponent(nameof(IsPublic), isPublic); + } + + var serverNameLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("ServerName")); + var serverNameBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), serverNameLabel.RectTransform, Anchor.CenterRight), + GameMain.Client.ServerSettings.ServerName) + { + OverflowClip = true, + MaxTextLength = NetConfig.ServerNameMaxLength + }; + serverNameBox.OnDeselected += (textBox, key) => + { + if (textBox.Text.IsNullOrWhiteSpace()) + { + textBox.Flash(GUIStyle.Red); + if (GameMain.Client != null) + { + textBox.Text = GameMain.Client.ServerSettings.ServerName; + } + } + GameMain.Client?.ServerSettings.ClientAdminWrite(NetFlags.Properties); + }; + AssignGUIComponent(nameof(ServerName), serverNameBox); + + // server message ************************************************************************* + + var motdHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("ServerMOTD")); + var motdCharacterCount = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), motdHeader.RectTransform, Anchor.CenterRight), string.Empty, textAlignment: Alignment.CenterRight); + var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), parent.RectTransform)) + { + Visible = true + }; + var serverMessageBox = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), + style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) + { + MaxTextLength = NetConfig.ServerMessageMaxLength + }; + var serverMessageHint = new GUITextBlock(new RectTransform(Vector2.One, serverMessageBox.RectTransform), + textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, text: TextManager.Get("ClickToWriteServerMessage")); + AssignGUIComponent(nameof(ServerMessageText), serverMessageBox); + + void updateServerMessageScrollBasedOnCaret() + { + float caretY = serverMessageBox.CaretScreenPos.Y; + float bottomCaretExtent = serverMessageBox.Font.LineHeight * 1.5f; + float topCaretExtent = -serverMessageBox.Font.LineHeight * 0.5f; + if (caretY + bottomCaretExtent > serverMessageContainer.Rect.Bottom) + { + serverMessageContainer.ScrollBar.BarScroll + = (caretY - serverMessageBox.Rect.Top - serverMessageContainer.Rect.Height + bottomCaretExtent) + / (serverMessageBox.Rect.Height - serverMessageContainer.Rect.Height); + } + else if (caretY + topCaretExtent < serverMessageContainer.Rect.Top) + { + serverMessageContainer.ScrollBar.BarScroll + = (caretY - serverMessageBox.Rect.Top + topCaretExtent) + / (serverMessageBox.Rect.Height - serverMessageContainer.Rect.Height); + } + } + serverMessageBox.OnSelected += (textBox, key) => + { + serverMessageHint.Visible = false; + updateServerMessageScrollBasedOnCaret(); + }; + serverMessageBox.OnTextChanged += (textBox, text) => + { + serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); + RefreshServerInfoSize(); + return true; + }; + serverMessageBox.RectTransform.SizeChanged += RefreshServerInfoSize; + motdCharacterCount.TextGetter += () => { return serverMessageBox.Text.Length + " / " + NetConfig.ServerMessageMaxLength; }; + + void RefreshServerInfoSize() + { + serverMessageHint.Visible = !serverMessageBox.Selected && !serverMessageBox.Readonly && string.IsNullOrWhiteSpace(serverMessageBox.Text); + Vector2 textSize = serverMessageBox.Font.MeasureString(serverMessageBox.WrappedText); + serverMessageBox.RectTransform.NonScaledSize = new Point(serverMessageBox.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10)); + serverMessageContainer.UpdateScrollBarSize(); + } + + serverMessageBox.OnEnterPressed += (textBox, text) => + { + string str = textBox.Text; + int caretIndex = textBox.CaretIndex; + textBox.Text = $"{str[..caretIndex]}\n{str[caretIndex..]}"; + textBox.CaretIndex = caretIndex + 1; + + return true; + }; + serverMessageBox.OnDeselected += (textBox, key) => + { + if (!textBox.Readonly) + { + GameMain.Client?.ServerSettings?.ClientAdminWrite(NetFlags.Properties); + } + serverMessageHint.Visible = !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); + }; + serverMessageBox.OnKeyHit += (sender, key) => updateServerMessageScrollBasedOnCaret(); + + // ************************************************************************* + + var playStyleLayoutLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), + TextManager.Get("ServerSettingsPlayStyle")); + var playStyleSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), playStyleLayoutLabel.RectTransform, Anchor.CenterRight)); + foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) + { + playStyleSelection.AddElement(playStyle, TextManager.Get("servertag." + playStyle), TextManager.Get("servertagdescription." + playStyle)); + } + AssignGUIComponent(nameof(PlayStyle), playStyleSelection); + + var passwordLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("Password")); + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), passwordLabel.RectTransform, Anchor.CenterRight), + TextManager.Get("ServerSettingsSetPassword"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => { CreateChangePasswordPrompt(); return true; } + }; + + var wrongPasswordBanBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), TextManager.Get("ServerSettingsBanAfterWrongPassword")); + AssignGUIComponent(nameof(BanAfterWrongPassword), wrongPasswordBanBox); + var allowedPasswordRetries = NetLobbyScreen.CreateLabeledNumberInput(parent, "ServerSettingsPasswordRetriesBeforeBan", 0, 10); + AssignGUIComponent(nameof(MaxPasswordRetriesBeforeBan), allowedPasswordRetries); + + var maxPlayers = NetLobbyScreen.CreateLabeledNumberInput(parent, "MaxPlayers", 0, NetConfig.MaxPlayers); + AssignGUIComponent(nameof(MaxPlayers), maxPlayers); + + // Language + var languageLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Language")); + var languageDD = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), languageLabel.RectTransform, Anchor.CenterRight)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDD.AddItem(language.Label, language.Identifier); + } + languageLabel.InheritTotalChildrenMinHeight(); + AssignGUIComponent(nameof(Language), languageDD); + + } + + private static void CreateChangePasswordPrompt() + { + var passwordMsgBox = new GUIMessageBox( + TextManager.Get("ServerSettingsSetPassword"), + "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, + relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); + var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), passwordMsgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); + var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform)) + { + Censor = true + }; + + passwordMsgBox.Content.Recalculate(); + passwordMsgBox.Content.InheritTotalChildrenHeight(); + passwordMsgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(passwordMsgBox.Content.RectTransform.MinSize.Y / passwordMsgBox.Content.RectTransform.RelativeSize.Y)); + + var okButton = passwordMsgBox.Buttons[0]; + okButton.OnClicked += (_, __) => + { + DebugConsole.ExecuteCommand($"setpassword \"{passwordBox.Text}\""); + return true; + }; + okButton.OnClicked += passwordMsgBox.Close; + + var cancelButton = passwordMsgBox.Buttons[1]; + cancelButton.OnClicked = (_, __) => + { + passwordMsgBox?.Close(); + passwordMsgBox = null; + return true; + }; + passwordBox.OnEnterPressed += (_, __) => + { + okButton.OnClicked.Invoke(okButton, okButton.UserData); + return true; + }; + + passwordBox.Select(); + } + + private void CreateGeneralTab(GUIComponent parent) + { + var listBox = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIListBoxNoBorder") + { + AutoHideScrollBar = true, + CurrentSelectMode = GUIListBox.SelectMode.None + }; + + NetLobbyScreen.CreateSubHeader("serversettingscategory.roundmanagement", listBox.Content); + + var endVoteBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsEndRoundVoting")); + AssignGUIComponent(nameof(AllowEndVoting), endVoteBox); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsEndRoundVotesRequired", tooltipTag: string.Empty, out var slider, out var sliderLabel); + + LocalizedString endRoundLabel = sliderLabel.Text; + slider.Step = 0.2f; + slider.Range = new Vector2(0.5f, 1.0f); + AssignGUIComponent(nameof(EndVoteRequiredRatio), slider); + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = endRoundLabel + " " + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; + return true; + }; + slider.OnMoved(slider, slider.BarScroll); + + //*********************************************** + + // Sub Selection + + var subSelectionLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsSubSelection")); + var subSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), subSelectionLabel.RectTransform, Anchor.CenterRight)); + foreach (SelectionMode selectionMode in Enum.GetValues(typeof(SelectionMode))) + { + subSelection.AddElement(selectionMode, TextManager.Get(selectionMode.ToString())); + } + AssignGUIComponent(nameof(SubSelectionMode), subSelection); + + // Mode Selection + var gameModeSelectionLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsModeSelection")); + var gameModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), gameModeSelectionLabel.RectTransform, Anchor.CenterRight)); + foreach (SelectionMode selectionMode in Enum.GetValues(typeof(SelectionMode))) + { + gameModeSelection.AddElement(selectionMode, TextManager.Get(selectionMode.ToString())); + } + AssignGUIComponent(nameof(ModeSelectionMode), gameModeSelection); + + //*********************************************** + + LocalizedString autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; + + var autorestartBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("AutoRestart")); + AssignGUIComponent(nameof(AutoRestart), autorestartBox); + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: string.Empty, tooltipTag: string.Empty, + out var startIntervalSlider, out var startIntervalSliderLabel, range: new Vector2(10.0f, 300.0f)); + startIntervalSlider.StepValue = 10.0f; + startIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = autoRestartDelayLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + AssignGUIComponent(nameof(AutoRestartInterval), startIntervalSlider); + startIntervalSlider.OnMoved(startIntervalSlider, startIntervalSlider.BarScroll); + + //*********************************************** + + var startWhenClientsReady = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsStartWhenClientsReady")); + AssignGUIComponent(nameof(StartWhenClientsReady), startWhenClientsReady); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsStartWhenClientsReadyRatio", tooltipTag: string.Empty, + out slider, out sliderLabel); + LocalizedString clientsReadyRequiredLabel = sliderLabel.Text; + slider.Step = 0.2f; + slider.Range = new Vector2(0.5f, 1.0f); + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = clientsReadyRequiredLabel.Replace("[percentage]", ((int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f)).ToString()); + return true; + }; + AssignGUIComponent(nameof(StartWhenClientsReadyRatio), slider); + slider.OnMoved(slider, slider.BarScroll); + + var randomizeLevelBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsRandomizeSeed")); + AssignGUIComponent(nameof(RandomizeSeed), randomizeLevelBox); + + //*********************************************** + + NetLobbyScreen.CreateSubHeader("serversettingsroundstab", listBox.Content); + + var voiceChatEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsVoiceChatEnabled")); + AssignGUIComponent(nameof(VoiceChatEnabled), voiceChatEnabled); + + var allowSpecBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsAllowSpectating")); + AssignGUIComponent(nameof(AllowSpectating), allowSpecBox); + + var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("LosEffect")); + var losModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 0.6f), losModeLabel.RectTransform, Anchor.CenterRight)); + foreach (var losMode in Enum.GetValues(typeof(LosMode)).Cast()) + { + losModeSelection.AddElement(losMode, TextManager.Get($"LosMode{losMode}"), TextManager.Get($"LosMode{losMode}.tooltip")); + } + AssignGUIComponent(nameof(LosMode), losModeSelection); + + var healthBarModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ShowEnemyHealthBars")); + var healthBarModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 0.6f), healthBarModeLabel.RectTransform, Anchor.CenterRight)); + foreach (var healthBarMode in Enum.GetValues(typeof(EnemyHealthBarMode)).Cast()) + { + healthBarModeSelection.AddElement(healthBarMode, TextManager.Get($"ShowEnemyHealthBars.{healthBarMode}")); + } + AssignGUIComponent(nameof(ShowEnemyHealthBars), healthBarModeSelection); + + var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); + AssignGUIComponent(nameof(DisableBotConversations), disableBotConversationsBox); + + //*********************************************** + + NetLobbyScreen.CreateSubHeader("serversettingscategory.misc", listBox.Content); + + var shareSubsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsShareSubFiles")); + AssignGUIComponent(nameof(AllowFileTransfers), shareSubsBox); + + var saveLogsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsSaveLogs")) + { + OnSelected = (GUITickBox) => + { + //TODO: fix? + //showLogButton.Visible = SaveServerLogs; + return true; + } + }; + AssignGUIComponent(nameof(SaveServerLogs), saveLogsBox); + + //-------------------------------------------------------------------------------- + // game settings + //-------------------------------------------------------------------------------- + + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + const string MonstersEnabledUserdata = "monstersenabled"; + const string ExtraCargoUserdata = "extracargo"; + + monstersEnabledButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), + TextManager.Get("ServerSettingsMonsterSpawns"), style: "GUIButtonSmall") + { + Enabled = !GameMain.NetworkMember.GameStarted + }; + monstersEnabledPanel = CreateMonstersEnabledPanel(); + monstersEnabledButton.UserData = MonstersEnabledUserdata; + monstersEnabledButton.OnClicked = ExtraSettingsButtonClicked; + + extraCargoButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonHolder.RectTransform), + TextManager.Get("ServerSettingsAdditionalCargo"), style: "GUIButtonSmall") + { + Enabled = !GameMain.NetworkMember.GameStarted + }; + + extraCargoPanel = CreateExtraCargoPanel(); + extraCargoButton.UserData = ExtraCargoUserdata; + extraCargoButton.OnClicked = ExtraSettingsButtonClicked; + + GUITextBlock.AutoScaleAndNormalize(buttonHolder.Children.Select(c => ((GUIButton)c).TextBlock)); + + bool ExtraSettingsButtonClicked(GUIButton button, object obj) + { + //the extra settings buttons (monsters enabled, cargo) hold a reference to the panel they're supposed to toggle + GUIComponent panel; + switch (obj as string) + { + case MonstersEnabledUserdata: + panel = monstersEnabledPanel; + break; + case ExtraCargoUserdata: + panel = extraCargoPanel; + break; + default: + throw new Exception("Unrecognized extra settings button"); + } + if (GameMain.NetworkMember.GameStarted) + { + panel.Visible = false; + button.Enabled = false; + return true; + } + panel.Visible = !panel.Visible; + return true; + } + } + + private GUIComponent CreateMonstersEnabledPanel() + { + var monsterFrame = new GUIListBox(new RectTransform(new Vector2(0.5f, 0.7f), settingsTabs[SettingsTab.General].RectTransform, Anchor.BottomLeft, Pivot.BottomRight)) + { + Visible = false, + IgnoreLayoutGroups = true + }; + + InitMonstersEnabled(); + List monsterNames = MonsterEnabled.Keys.ToList(); + tempMonsterEnabled = new Dictionary(MonsterEnabled); + foreach (Identifier s in monsterNames) + { + LocalizedString translatedLabel = TextManager.Get($"Character.{s}").Fallback(s.Value); + var monsterEnabledBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), monsterFrame.Content.RectTransform) { MinSize = new Point(0, 25) }, + label: translatedLabel) + { + Selected = tempMonsterEnabled[s], + OnSelected = (GUITickBox tb) => + { + tempMonsterEnabled[s] = tb.Selected; + return true; + } + }; + } + monsterFrame.Content.RectTransform.SortChildren((c1, c2) => + { + var name1 = (c1.GUIComponent as GUITickBox)?.Text ?? string.Empty; + var name2 = (c2.GUIComponent as GUITickBox)?.Text ?? string.Empty; + return name1.CompareTo(name2); + }); + + if (GameMain.Client == null || + !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) + { + SetElementInteractability(monsterFrame.Content, false); + } + + return monsterFrame; + } + + private GUIComponent CreateExtraCargoPanel() + { + var cargoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), settingsTabs[SettingsTab.General].RectTransform, Anchor.BottomRight, Pivot.BottomLeft)) + { + Visible = false, + IgnoreLayoutGroups = true + }; + 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; + }; + + foreach (ItemPrefab ip in ItemPrefab.Prefabs.OrderBy(ip => ip.Name)) + { + if (ip.AllowAsExtraCargo.HasValue) + { + if (!ip.AllowAsExtraCargo.Value) { continue; } + } + else + { + if (!ip.CanBeBought) { continue; } + } + + var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoList.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true) + { + Stretch = true, + UserData = ip, + RelativeSpacing = 0.05f + }; + + if (ip.InventoryIcon != null || ip.Sprite != null) + { + GUIImage img = new GUIImage(new RectTransform(new Point(itemFrame.Rect.Height), itemFrame.RectTransform), + ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) + { + CanBeFocused = false + }; + img.Color = img.Sprite == ip.InventoryIcon ? ip.InventoryIconColor : ip.SpriteColor; + } + + new GUITextBlock(new RectTransform(new Vector2(0.75f, 1.0f), itemFrame.RectTransform), + ip.Name, font: GUIStyle.SmallFont) + { + Wrap = true, + CanBeFocused = false + }; + + ExtraCargo.TryGetValue(ip, out int cargoVal); + var amountInput = new GUINumberInput(new RectTransform(new Vector2(0.35f, 1.0f), itemFrame.RectTransform), + NumberType.Int, textAlignment: Alignment.CenterLeft) + { + MinValueInt = 0, + MaxValueInt = MaxExtraCargoItemsOfType, + IntValue = cargoVal + }; + amountInput.OnValueChanged += (numberInput) => + { + if (ExtraCargo.ContainsKey(ip)) + { + ExtraCargo[ip] = numberInput.IntValue; + if (numberInput.IntValue <= 0) { ExtraCargo.Remove(ip); } + } + else if (ExtraCargo.Keys.Count < MaxExtraCargoItemTypes) + { + ExtraCargo.Add(ip, numberInput.IntValue); + } + numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0; + CoroutineManager.Invoke(() => + { + foreach (var child in cargoList.Content.GetAllChildren()) + { + if (child.GetChild() is GUINumberInput otherNumberInput) + { + otherNumberInput.PlusButton.Enabled = ExtraCargo.Keys.Count < MaxExtraCargoItemTypes && otherNumberInput.IntValue < otherNumberInput.MaxValueInt; + } + } + }, 0.0f); + }; + } + if (GameMain.Client == null || + !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) + { + SetElementInteractability(cargoList.Content, false); + } + + return cargoFrame; + } + + private void CreateAntigriefingTab(GUIComponent parent) + { + var listBox = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform), style: "GUIListBoxNoBorder") + { + AutoHideScrollBar = true, + CurrentSelectMode = GUIListBox.SelectMode.None + }; + + //-------------------------------------------------------------------------------- + // antigriefing + //-------------------------------------------------------------------------------- + + var tickBoxContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), listBox.Content.RectTransform)) + { + AutoHideScrollBar = true, + UseGridLayout = true + }; + tickBoxContainer.Padding *= 2.0f; + + var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowFriendlyFire")); + AssignGUIComponent(nameof(AllowFriendlyFire), allowFriendlyFire); + + var killableNPCs = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsKillableNPCs")); + AssignGUIComponent(nameof(KillableNPCs), killableNPCs); + + var destructibleOutposts = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsDestructibleOutposts")); + AssignGUIComponent(nameof(DestructibleOutposts), destructibleOutposts); + + var lockAllDefaultWires = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsLockAllDefaultWires")); + AssignGUIComponent(nameof(LockAllDefaultWires), lockAllDefaultWires); + + var allowRewiring = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowRewiring")); + AssignGUIComponent(nameof(AllowRewiring), allowRewiring); + + var allowWifiChatter = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowWifiChat")); + AssignGUIComponent(nameof(AllowLinkingWifiToChat), allowWifiChatter); + + var allowDisguises = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowDisguises")); + AssignGUIComponent(nameof(AllowDisguises), allowDisguises); + + var allowImmediateItemDeliveryBox = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsImmediateItemDelivery")); + AssignGUIComponent(nameof(AllowImmediateItemDelivery), allowImmediateItemDeliveryBox); + + GUITextBlock.AutoScaleAndNormalize(tickBoxContainer.Content.Children.Select(c => ((GUITickBox)c).TextBlock)); + + tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); + + tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); + + var voteKickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform), + TextManager.Get("ServerSettingsAllowVoteKick")); + AssignGUIComponent(nameof(AllowVoteKick), voteKickBox); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsKickVotesRequired", tooltipTag: string.Empty, out var slider, out var sliderLabel); + LocalizedString votesRequiredLabel = sliderLabel.Text + " "; + slider.Step = 0.2f; + slider.Range = new Vector2(0.5f, 1.0f); + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = votesRequiredLabel + (int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f) + " %"; + return true; + }; + AssignGUIComponent(nameof(KickVoteRequiredRatio), slider); + slider.OnMoved(slider, slider.BarScroll); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsAutobanTime", tooltipTag: "ServerSettingsAutobanTime.Tooltip", out slider, out sliderLabel); + LocalizedString autobanLabel = sliderLabel.Text + " "; + slider.Range = new Vector2(0.0f, MaxAutoBanTime); + slider.StepValue = 60.0f * 15.0f; //15 minutes + slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + ((GUITextBlock)scrollBar.UserData).Text = autobanLabel + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + AssignGUIComponent(nameof(AutoBanTime), slider); + slider.OnMoved(slider, slider.BarScroll); + + var maximumTransferAmount = NetLobbyScreen.CreateLabeledNumberInput(listBox.Content, "serversettingsmaximumtransferrequest", 0, CampaignMode.MaxMoney, "serversettingsmaximumtransferrequesttooltip"); + AssignGUIComponent(nameof(MaximumMoneyTransferRequest), maximumTransferAmount); + + var lootedMoneyDestination = NetLobbyScreen.CreateLabeledDropdown(listBox.Content, "serversettingslootedmoneydestination", numElements: 2, "serversettingslootedmoneydestinationtooltip"); + lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.bank"), LootedMoneyDestination.Bank); + lootedMoneyDestination.AddItem(TextManager.Get("lootedmoneydestination.wallet"), LootedMoneyDestination.Wallet); + AssignGUIComponent(nameof(LootedMoneyDestination), lootedMoneyDestination); + + var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.0f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection")) + { + ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip") + }; + AssignGUIComponent(nameof(EnableDoSProtection), enableDosProtection); + + NetLobbyScreen.CreateLabeledSlider(listBox.Content, headerTag: string.Empty, valueLabelTag: "ServerSettingsMaxPacketAmount", tooltipTag: string.Empty, out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel); + LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text; + maxPacketSlider.Step = 0.001f; + maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax); + maxPacketSlider.ToolTip = packetAmountTooltip; + maxPacketSlider.OnMoved = (scrollBar, _) => + { + GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData; + int value = (int)MathF.Floor(scrollBar.BarScrollValue); + + LocalizedString valueText = value > PacketLimitMin + ? value.ToString() + : TextManager.Get("ServerSettingsNoLimit"); + + switch (value) + { + case <= PacketLimitMin: + textBlock.TextColor = GUIStyle.Green; + scrollBar.ToolTip = packetAmountTooltip; + break; + case < PacketLimitWarning: + textBlock.TextColor = GUIStyle.Red; + scrollBar.ToolTip = packetAmountTooltipWarning; + break; + default: + textBlock.TextColor = GUIStyle.TextColorNormal; + scrollBar.ToolTip = packetAmountTooltip; + break; + } + + textBlock.Text = $"{maxPacketCountLabel} {valueText}"; + return true; + }; + AssignGUIComponent(nameof(MaxPacketAmount), maxPacketSlider); + maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll); + + // karma -------------------------------------------------------------------------- + + NetLobbyScreen.CreateSubHeader("Karma", listBox.Content, toolTipTag: "KarmaExplanation"); + + var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), listBox.Content.RectTransform), TextManager.Get("ServerSettingsUseKarma")) + { + ToolTip = TextManager.Get("KarmaExplanation") + }; + AssignGUIComponent(nameof(KarmaEnabled), karmaBox); + + karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), listBox.Content.RectTransform)); + foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) + { + karmaPresetDD.AddItem(TextManager.Get("KarmaPreset." + karmaPreset), karmaPreset); + } + karmaElements.Add(karmaPresetDD); + + var karmaSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), listBox.Content.RectTransform), style: null); + karmaElements.Add(karmaSettingsContainer); + karmaSettingsList = new GUIListBox(new RectTransform(Vector2.One, karmaSettingsContainer.RectTransform)) + { + Spacing = (int)(8 * GUI.Scale) + }; + karmaSettingsList.Padding *= 2.0f; + + karmaPresetDD.SelectItem(KarmaPreset); + SetElementInteractability(karmaSettingsList.Content, !karmaBox.Selected || KarmaPreset != "custom"); + GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); + karmaPresetDD.OnSelected = (selected, obj) => + { + string newKarmaPreset = obj as string; + if (newKarmaPreset == KarmaPreset) { return true; } + + List properties = netProperties.Values.ToList(); + List prevValues = new List(); + foreach (NetPropertyData prop in netProperties.Values) + { + prevValues.Add(prop.TempValue); + if (prop.GUIComponent != null) { prop.Value = prop.GUIComponentValue; } + } + if (KarmaPreset == "custom") + { + GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); + GameMain.NetworkMember?.KarmaManager?.Save(); + } + KarmaPreset = newKarmaPreset; + GameMain.NetworkMember.KarmaManager.SelectPreset(KarmaPreset); + karmaSettingsList.Content.ClearChildren(); + GameMain.NetworkMember.KarmaManager.CreateSettingsFrame(karmaSettingsList.Content); + SetElementInteractability(karmaSettingsList.Content, !karmaBox.Selected || KarmaPreset != "custom"); + for (int i = 0; i < netProperties.Count; i++) + { + properties[i].TempValue = prevValues[i]; + } + return true; + }; + AssignGUIComponent(nameof(KarmaPreset), karmaPresetDD); + karmaBox.OnSelected = (tb) => + { + SetElementInteractability(karmaSettingsList.Content, !karmaBox.Selected || KarmaPreset != "custom"); + karmaElements.ForEach(e => e.Visible = tb.Selected); + return true; + }; + karmaElements.ForEach(e => e.Visible = KarmaEnabled); + + listBox.Content.InheritTotalChildrenMinHeight(); + } + + private void CreateBanlistTab(GUIComponent parent) + { + BanList.CreateBanFrame(parent); + } + + private bool SelectSettingsTab(GUIButton button, object obj) + { + selectedTab = (SettingsTab)obj; + foreach (var key in settingsTabs.Keys) + { + settingsTabs[key].Visible = key == selectedTab; + tabButtons[key].Selected = key == selectedTab; + } + return true; + } + + public void Close() + { + if (KarmaPreset == "custom") + { + GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); + GameMain.NetworkMember?.KarmaManager?.Save(); + } + ClientAdminWrite(NetFlags.Properties); + foreach (NetPropertyData prop in netProperties.Values) + { + prop.GUIComponent = null; + } + settingsFrame = null; + //give control of server settings back to elements in the lobby + GameMain.NetLobbyScreen.AssignComponentsToServerSettings(); + } + + public bool ToggleSettingsFrame(GUIButton button, object obj) + { + if (GameMain.NetworkMember == null) { return false; } + if (settingsFrame == null) + { + CreateSettingsFrame(); + } + else + { + Close(); + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 23b4c5b10..c3dbcca81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -275,7 +275,7 @@ namespace Barotrauma.Networking var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; } - GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); + GameMain.Client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } //encode audio and enqueue it lock (buffers) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 6c664b86d..f87161645 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Networking if (client.VoipSound == null) { DebugConsole.Log("Recreating voipsound " + queueId); - client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); + client.VoipSound = new VoipSound(client, GameMain.SoundManager, client.VoipQueue); } GameMain.SoundManager.ForceStreamUpdate(); client.RadioNoise = 0.0f; @@ -122,12 +122,13 @@ namespace Barotrauma.Networking ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) && senderRadio.CanReceive(recipientRadio) ? ChatMessageType.Radio : ChatMessageType.Default; - client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); + client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.RadioNoise = 0.0f; if (messageType == ChatMessageType.Radio) { + client.VoipSound.UsingRadio = true; client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier); if (distanceFactor > RangeNear && !spectating) { @@ -137,11 +138,12 @@ namespace Barotrauma.Networking } else { - client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier); + client.VoipSound.UsingRadio = false; + client.VoipSound.SetRange(ChatMessage.SpeakRangeVOIP * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRangeVOIP * speechImpedimentMultiplier * rangeMultiplier); } client.VoipSound.UseMuffleFilter = messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters && - SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRange, client.Character.CurrentHull); + SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRangeVOIP, client.Character.CurrentHull); } GameMain.NetLobbyScreen?.SetPlayerSpeaking(client); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 9d762e0d0..acea82954 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -70,6 +70,8 @@ namespace Barotrauma.Particles public Vector4 ColorMultiplier; + public float VelocityChangeMultiplier; + public bool DrawOnTop { get; private set; } public ParticlePrefab.DrawTargetType DrawTarget @@ -121,8 +123,8 @@ namespace Barotrauma.Particles animState = 0; animFrame = 0; - - currentHull = Hull.FindHull(position, hullGuess); + + currentHull = prefab.CanEnterSubs ? Hull.FindHull(position, hullGuess) : null; size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f); @@ -178,6 +180,8 @@ namespace Barotrauma.Particles HighQualityCollisionDetection = false; + VelocityChangeMultiplier = 1.0f; + OnChangeHull = null; OnCollision = null; @@ -247,8 +251,8 @@ namespace Barotrauma.Particles bool inWater = (currentHull == null || (currentHull.Submarine != null && position.Y - currentHull.Submarine.DrawPosition.Y < currentHull.Surface)); if (inWater) { - velocity.X += velocityChangeWater.X * deltaTime; - velocity.Y += velocityChangeWater.Y * deltaTime; + velocity.X += velocityChangeWater.X * VelocityChangeMultiplier * deltaTime; + velocity.Y += velocityChangeWater.Y * VelocityChangeMultiplier * deltaTime; if (prefab.WaterDrag > 0.0f) { ApplyDrag(prefab.WaterDrag, deltaTime); @@ -256,8 +260,8 @@ namespace Barotrauma.Particles } else { - velocity.X += velocityChange.X * deltaTime; - velocity.Y += velocityChange.Y * deltaTime; + velocity.X += velocityChange.X * VelocityChangeMultiplier * deltaTime; + velocity.Y += velocityChange.Y * VelocityChangeMultiplier * deltaTime; if (prefab.Drag > 0.0f) { ApplyDrag(prefab.Drag, deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 050ee9ded..c120eca23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -132,6 +132,9 @@ namespace Barotrauma.Particles } } + [Editable, Serialize(true, IsPropertySaveable.No, description: "Is the particle considered to be inside a submarine if it spawns at a position inside a hull (causing it to move with the sub)?")] + public bool CanEnterSubs { get; private set; } + [Editable(0.0f, 10000.0f), Serialize(0.0f, IsPropertySaveable.No, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")] public float CollisionRadius { get; private set; } @@ -153,10 +156,10 @@ namespace Barotrauma.Particles //size ----------------------------------------- - [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The minimum initial size of the particle.")] + [Editable(DecimalCount = 3), Serialize("1.0,1.0", IsPropertySaveable.No, description: "The minimum initial size of the particle.")] public Vector2 StartSizeMin { get; private set; } - [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The maximum initial size of the particle.")] + [Editable(DecimalCount = 3), Serialize("1.0,1.0", IsPropertySaveable.No, description: "The maximum initial size of the particle.")] public Vector2 StartSizeMax { get; private set; } [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index ec8146ae0..b081a7449 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -1,12 +1,10 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.IO; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Collections.Immutable; -using System.Globalization; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -56,12 +54,16 @@ namespace Barotrauma return null; } - var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") + var saveFrame = new GUIFrame( + new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, + style: "ListBoxElement") { UserData = saveInfo }; - var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath), + var nameText = new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), + Path.GetFileNameWithoutExtension(saveInfo.FilePath), textColor: GUIStyle.TextColorBright) { CanBeFocused = false @@ -79,8 +81,10 @@ namespace Barotrauma prevSaveFiles ??= new List(); prevSaveFiles.Add(saveInfo); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: saveInfo.SubmarineName, font: GUIStyle.SmallFont) + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), + text: saveInfo.SubmarineName, + font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = saveInfo.FilePath @@ -91,8 +95,11 @@ namespace Barotrauma { saveTimeStr = time.ToLocalUserString(); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTimeStr, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) + new GUITextBlock( + new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), + text: saveTimeStr, + textAlignment: Alignment.Right, + font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = saveInfo.FilePath @@ -127,9 +134,19 @@ namespace Barotrauma public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; - public SettingValue StartingFunds; - public SettingValue Difficulty; + public SettingValue StartingFunds; + public SettingValue WorldHostility; public SettingValue StartItemSet; + public SettingValue CrewVitalityMultiplier; + public SettingValue NonCrewVitalityMultiplier; + public SettingValue OxygenMultiplier; + public SettingValue FuelMultiplier; + public SettingValue MissionRewardMultiplier; + public SettingValue ShopPriceMultiplier; + public SettingValue ShipyardPriceMultiplier; + public SettingValue RepairFailMultiplier; + public SettingValue PatdownProbability; + public SettingValue ShowHuskWarning; public readonly CampaignSettings CreateSettings() { @@ -140,8 +157,18 @@ namespace Barotrauma RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), StartingBalanceAmount = StartingFunds.GetValue(), - Difficulty = Difficulty.GetValue(), - StartItemSet = StartItemSet.GetValue() + WorldHostility = WorldHostility.GetValue(), + StartItemSet = StartItemSet.GetValue(), + CrewVitalityMultiplier = CrewVitalityMultiplier.GetValue(), + NonCrewVitalityMultiplier = NonCrewVitalityMultiplier.GetValue(), + OxygenMultiplier = OxygenMultiplier.GetValue(), + FuelMultiplier = FuelMultiplier.GetValue(), + MissionRewardMultiplier = MissionRewardMultiplier.GetValue(), + ShopPriceMultiplier = ShopPriceMultiplier.GetValue(), + ShipyardPriceMultiplier = ShipyardPriceMultiplier.GetValue(), + RepairFailMultiplier = RepairFailMultiplier.GetValue(), + PatdownProbability = PatdownProbability.GetValue(), + ShowHuskWarning = ShowHuskWarning.GetValue(), }; } } @@ -188,9 +215,16 @@ namespace Barotrauma bool loadingPreset = false; - GUILayoutGroup presetDropdownLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), TextManager.Get("campaignsettingpreset")); - GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length + 1); + GUILayoutGroup presetDropdownLayout = new GUILayoutGroup( + new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft); + new GUITextBlock( + new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), + TextManager.Get("campaignsettingpreset")); + GUIDropDown presetDropdown = new GUIDropDown( + new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), + elementCount: CampaignModePresets.List.Length + 1); presetDropdown.AddItem(TextManager.Get("karmapreset.custom"), null); presetDropdown.Select(0); @@ -216,37 +250,235 @@ namespace Barotrauma Spacing = GUI.IntScale(5) }; - SettingValue tutorialEnabled = isSinglePlayer ? - CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize, OnValuesChanged) : - new SettingValue(static () => false, static _ => { }); - SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize, OnValuesChanged); + // GENERAL CAMPAIGN SETTINGS: - ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); - SettingCarouselElement prevStartingSet = startingSetOptions.FirstOrNull(element => element.Value == prevSettings.StartItemSet) ?? startingSetOptions[1]; - SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions, OnValuesChanged); + NetLobbyScreen.CreateSubHeader("campaignsettingcategories.general", settingsList.Content); - ImmutableArray> fundOptions = ImmutableArray.Create( - new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low"), - new SettingCarouselElement(StartingBalanceAmount.Medium, "startingfunds.medium"), - new SettingCarouselElement(StartingBalanceAmount.High, "startingfunds.high") + // Tutorial + SettingValue tutorialEnabled = isSinglePlayer + ? CreateTickbox( + settingsList.Content, + TextManager.Get("CampaignOption.EnableTutorial"), + TextManager.Get("campaignoption.enabletutorial.tooltip"), + prevSettings.TutorialEnabled, + verticalSize, + OnValuesChanged) + : new SettingValue(static () => false, static _ => { }); + + // Jovian radiation + SettingValue radiationEnabled = CreateTickbox( + settingsList.Content, + TextManager.Get("CampaignOption.EnableRadiation"), + TextManager.Get("campaignoption.enableradiation.tooltip"), + prevSettings.RadiationEnabled, + verticalSize, + OnValuesChanged); + + // RESOURCE-RELATED CAMPAIGN SETTINGS: + + NetLobbyScreen.CreateSubHeader("campaignsettingcategories.resources", settingsList.Content); + + // Starting set + ImmutableArray> startingSetOptions = + StartItemSet.Sets + .OrderBy(s => s.Order) + .Select(set => new SettingCarouselElement( + set.Identifier, + $"startitemset.{set.Identifier}")) + .ToImmutableArray(); + SettingCarouselElement prevStartingSet = startingSetOptions + .FirstOrNull(element => element.Value == prevSettings.StartItemSet) + ?? startingSetOptions[1]; + SettingValue startingSetInput = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("startitemset"), + TextManager.Get("startitemsettooltip"), + prevStartingSet, + verticalSize, + startingSetOptions, + OnValuesChanged); + + // Starting money + ImmutableArray> fundOptions = ImmutableArray.Create( + new SettingCarouselElement(StartingBalanceAmountOption.Low, "startingfunds.low"), + new SettingCarouselElement(StartingBalanceAmountOption.Medium, "startingfunds.medium"), + new SettingCarouselElement(StartingBalanceAmountOption.High, "startingfunds.high") ); + SettingCarouselElement prevStartingFund = fundOptions + .FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) + ?? fundOptions[1]; + SettingValue startingFundsInput = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("startingfundsdescription"), + TextManager.Get("startingfundstooltip"), + prevStartingFund, + verticalSize, + fundOptions, + OnValuesChanged); - SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; - SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions, OnValuesChanged); - - ImmutableArray> difficultyOptions = ImmutableArray.Create( - new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), - new SettingCarouselElement(GameDifficulty.Medium, "difficulty.medium"), - new SettingCarouselElement(GameDifficulty.Hard, "difficulty.hard"), - new SettingCarouselElement(GameDifficulty.Hellish, "difficulty.hellish", isHidden: true) - ); - - SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; - SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions, OnValuesChanged); - - SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), + // Max mission count + SettingValue maxMissionCountInput = CreateGUIIntegerInputCarousel( + settingsList.Content, + TextManager.Get("maxmissioncount"), + TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, - valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, + valueStep: 1, + minValue: CampaignSettings.MinMissionCountLimit, + maxValue: CampaignSettings.MaxMissionCountLimit, + verticalSize, + OnValuesChanged); + + // Mission reward multiplier + CampaignSettings.MultiplierSettings rewardMultiplierSettings = CampaignSettings.GetMultiplierSettings("MissionRewardMultiplier"); + SettingValue rewardMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.missionrewardmultiplier"), + TextManager.Get("campaignoption.missionrewardmultiplier.tooltip"), + prevSettings.MissionRewardMultiplier, + valueStep: rewardMultiplierSettings.Step, + minValue: rewardMultiplierSettings.Min, + maxValue: rewardMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Shop buying prices multiplier + CampaignSettings.MultiplierSettings shopPriceMultiplierSettings = CampaignSettings.GetMultiplierSettings("ShopPriceMultiplier"); + SettingValue shopPriceMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.shoppricemultiplier"), + TextManager.Get("campaignoption.shoppricemultiplier.tooltip"), + prevSettings.ShopPriceMultiplier, + valueStep: shopPriceMultiplierSettings.Step, + minValue: shopPriceMultiplierSettings.Min, + maxValue: shopPriceMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Shipyard prices multiplier + CampaignSettings.MultiplierSettings shipyardPriceMultiplierSettings = CampaignSettings.GetMultiplierSettings("ShipyardPriceMultiplier"); + SettingValue shipyardPriceMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.shipyardpricemultiplier"), + TextManager.Get("campaignoption.shipyardpricemultiplier.tooltip"), + prevSettings.ShipyardPriceMultiplier, + valueStep: shipyardPriceMultiplierSettings.Step, + minValue: shipyardPriceMultiplierSettings.Min, + maxValue: shipyardPriceMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // OVERALL HAZARD-RELATED CAMPAIGN SETTINGS: + + NetLobbyScreen.CreateSubHeader("campaignsettingcategories.hazards", settingsList.Content); + + // World hostility (used to be "Difficulty" or level difficulty) + ImmutableArray> hostilityOptions = ImmutableArray.Create( + new SettingCarouselElement(WorldHostilityOption.Low, "worldhostility.low"), + new SettingCarouselElement(WorldHostilityOption.Medium, "worldhostility.medium"), + new SettingCarouselElement(WorldHostilityOption.High, "worldhostility.high"), + new SettingCarouselElement(WorldHostilityOption.Hellish, "worldhostility.hellish", isHidden: true) + ); + SettingCarouselElement prevHostility = hostilityOptions + .FirstOrNull(element => element.Value == prevSettings.WorldHostility) + ?? hostilityOptions[1]; + SettingValue hostilityInput = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("worldhostility"), + TextManager.Get("worldhostility.tooltip"), + prevHostility, + verticalSize, + hostilityOptions, + OnValuesChanged); + + // Crew max vitality multiplier + CampaignSettings.MultiplierSettings crewVitalityMultiplierSettings = CampaignSettings.GetMultiplierSettings("CrewVitalityMultiplier"); + SettingValue crewVitalityMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.maxvitalitymultipliercrew"), + TextManager.Get("campaignoption.maxvitalitymultipliercrew.tooltip"), + prevSettings.CrewVitalityMultiplier, + valueStep: crewVitalityMultiplierSettings.Step, + minValue: crewVitalityMultiplierSettings.Min, + maxValue: crewVitalityMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Non-crew max vitality multiplier + CampaignSettings.MultiplierSettings nonCrewVitalityMultiplierSettings = CampaignSettings.GetMultiplierSettings("NonCrewVitalityMultiplier"); + SettingValue nonCrewVitalityMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.maxvitalitymultipliernoncrew"), + TextManager.Get("campaignoption.maxvitalitymultipliernoncrew.tooltip"), + prevSettings.NonCrewVitalityMultiplier, + valueStep: nonCrewVitalityMultiplierSettings.Step, + minValue: nonCrewVitalityMultiplierSettings.Min, + maxValue: nonCrewVitalityMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Oxygen source multiplier + CampaignSettings.MultiplierSettings oxygenSourceMultiplierSettings = CampaignSettings.GetMultiplierSettings("OxygenMultiplier"); + SettingValue oxygenMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.oxygensourcemultiplier"), + TextManager.Get("campaignoption.oxygensourcemultiplier.tooltip"), + prevSettings.OxygenMultiplier, + valueStep: oxygenSourceMultiplierSettings.Step, + minValue: oxygenSourceMultiplierSettings.Min, + maxValue: oxygenSourceMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Reactor fuel multiplier + CampaignSettings.MultiplierSettings reactorFuelMultiplierSettings = CampaignSettings.GetMultiplierSettings("FuelMultiplier"); + SettingValue fuelMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.reactorfuelmultiplier"), + TextManager.Get("campaignoption.reactorfuelmultiplier.tooltip"), + prevSettings.FuelMultiplier, + valueStep: reactorFuelMultiplierSettings.Step, + minValue: reactorFuelMultiplierSettings.Min, + maxValue: reactorFuelMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + // Repair fail effect multiplier + CampaignSettings.MultiplierSettings repairFailMultiplierSettings = CampaignSettings.GetMultiplierSettings("RepairFailMultiplier"); + SettingValue repairFailMultiplier = CreateGUIFloatInputCarousel( + settingsList.Content, + TextManager.Get("campaignoption.repairfailmultiplier"), + TextManager.Get("campaignoption.repairfailmultiplier.tooltip"), + prevSettings.RepairFailMultiplier, + valueStep: repairFailMultiplierSettings.Step, + minValue: repairFailMultiplierSettings.Min, + maxValue: repairFailMultiplierSettings.Max, + verticalSize, + OnValuesChanged); + + ImmutableArray> patdownProbabilityPresets = ImmutableArray.Create( + new SettingCarouselElement(PatdownProbabilityOption.Off, "probability.off"), + new SettingCarouselElement(PatdownProbabilityOption.Low, "probability.low"), + new SettingCarouselElement(PatdownProbabilityOption.Medium, "probability.medium"), + new SettingCarouselElement(PatdownProbabilityOption.High, "probability.high") + ); + SettingCarouselElement prevPatdownProbability = patdownProbabilityPresets + .FirstOrNull(element => element.Value == prevSettings.PatdownProbability) + ?? patdownProbabilityPresets[1]; // middle option + SettingValue patdownProbability = CreateSelectionCarousel( + settingsList.Content, + TextManager.Get("campaignoption.patdownprobability"), + TextManager.Get("campaignoption.patdownprobability.tooltip"), + prevPatdownProbability, + verticalSize, + patdownProbabilityPresets, + OnValuesChanged); + + // Show initial husk warning + SettingValue huskWarning = CreateTickbox( + settingsList.Content, + TextManager.Get("campaignoption.showhuskwarning"), + TextManager.Get("campaignoption.showhuskwarning.tooltip"), + prevSettings.ShowHuskWarning, verticalSize, OnValuesChanged); @@ -259,8 +491,18 @@ namespace Barotrauma radiationEnabled.SetValue(settings.RadiationEnabled); maxMissionCountInput.SetValue(settings.MaxMissionCount); startingFundsInput.SetValue(settings.StartingBalanceAmount); - difficultyInput.SetValue(settings.Difficulty); + hostilityInput.SetValue(settings.WorldHostility); startingSetInput.SetValue(settings.StartItemSet); + crewVitalityMultiplier.SetValue(settings.CrewVitalityMultiplier); + nonCrewVitalityMultiplier.SetValue(settings.NonCrewVitalityMultiplier); + oxygenMultiplier.SetValue(settings.OxygenMultiplier); + fuelMultiplier.SetValue(settings.FuelMultiplier); + rewardMultiplier.SetValue(settings.MissionRewardMultiplier); + shopPriceMultiplier.SetValue(settings.ShopPriceMultiplier); + shipyardPriceMultiplier.SetValue(settings.ShipyardPriceMultiplier); + repairFailMultiplier.SetValue(settings.RepairFailMultiplier); + patdownProbability.SetValue(settings.PatdownProbability); + huskWarning.SetValue(settings.ShowHuskWarning); loadingPreset = false; return true; }; @@ -268,7 +510,7 @@ namespace Barotrauma void OnValuesChanged() { if (loadingPreset) { return; } - presetDropdown.Select(0); + presetDropdown.Select(0); // Switch to the Custom preset if this is an actual user-made change } return new CampaignSettingElements @@ -278,70 +520,178 @@ namespace Barotrauma RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, StartingFunds = startingFundsInput, - Difficulty = difficultyInput, - StartItemSet = startingSetInput + WorldHostility = hostilityInput, + StartItemSet = startingSetInput, + CrewVitalityMultiplier = crewVitalityMultiplier, + NonCrewVitalityMultiplier = nonCrewVitalityMultiplier, + OxygenMultiplier = oxygenMultiplier, + FuelMultiplier = fuelMultiplier, + MissionRewardMultiplier = rewardMultiplier, + ShopPriceMultiplier = shopPriceMultiplier, + ShipyardPriceMultiplier = shipyardPriceMultiplier, + RepairFailMultiplier = repairFailMultiplier, + PatdownProbability = patdownProbability, + ShowHuskWarning = huskWarning, }; - // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox - static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize, Action onChanged) + // Create a number input with plus and minus buttons because for some reason + // the default GUINumberInput buttons don't work when in a GUIMessageBox + static SettingValue CreateGUIIntegerInputCarousel( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + int defaultValue, + int valueStep, + int minValue, + int maxValue, + float verticalSize, + Action onChanged) { - GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + GUILayoutGroup inputContainer = CreateSettingBase( + parent, + description, + tooltip, + horizontalSize: 0.55f, + verticalSize: verticalSize); - GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) - { - ClickSound = GUISoundType.Decrease, - UserData = -valueStep - }; - GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputContainer.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", - hidePlusMinusButtons: true) + GUIButton minusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIMinusButton", + textAlignment: Alignment.Center); + RectTransform numberInputRect = new(Vector2.One, inputContainer.RectTransform, Anchor.Center); + GUIButton plusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIPlusButton", + textAlignment: Alignment.Center); + GUINumberInput numberInput = new GUINumberInput( + numberInputRect, + NumberType.Int, + textAlignment: Alignment.Center, + style: "GUITextBox", + buttonVisibility: GUINumberInput.ButtonVisibility.ForceVisible, + customPlusMinusButtons: (plusButton, minusButton)) { IntValue = defaultValue, MinValueInt = minValue, - MaxValueInt = maxValue + MaxValueInt = maxValue, + ValueStep = valueStep, + ToolTip = tooltip }; inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); - GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) - { - ClickSound = GUISoundType.Increase, - UserData = valueStep - }; - - minusButton.OnClicked = plusButton.OnClicked = ChangeValue; numberInput.OnValueChanged += _ => onChanged(); - bool ChangeValue(GUIButton btn, object userData) - { - if (userData is not int change) { return false; } - - numberInput.IntValue += change; - return true; - } - - return new SettingValue(() => numberInput.IntValue, i => numberInput.IntValue = i); + return new SettingValue( + () => numberInput.IntValue, + i => numberInput.IntValue = i); } - static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, - ImmutableArray> options, Action onChanged) + static SettingValue CreateGUIFloatInputCarousel( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + float defaultValue, + float valueStep, + float minValue, + float maxValue, + float verticalSize, + Action onChanged) { - GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); + GUILayoutGroup inputContainer = CreateSettingBase( + parent, + description, + tooltip, + horizontalSize: 0.55f, + verticalSize: verticalSize); - GUIButton minusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonToggleLeft", textAlignment: Alignment.Center) { UserData = -1 }; - GUIFrame inputFrame = new GUIFrame(new RectTransform(Vector2.One, inputContainer.RectTransform), style: null); - GUINumberInput numberInput = new GUINumberInput(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), NumberType.Int, textAlignment: Alignment.Center, style: "GUITextBox", hidePlusMinusButtons: true) + GUIButton minusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIMinusButton", + textAlignment: Alignment.Center); + RectTransform numberInputRect = new(Vector2.One, inputContainer.RectTransform, Anchor.Center); + GUIButton plusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIPlusButton", + textAlignment: Alignment.Center); + GUINumberInput numberInput = new GUINumberInput( + numberInputRect, + NumberType.Float, + textAlignment: Alignment.Center, + style: "GUITextBox", + buttonVisibility: GUINumberInput.ButtonVisibility.ForceVisible, + customPlusMinusButtons: (plusButton, minusButton)) + { + FloatValue = defaultValue, + MinValueFloat = minValue, + MaxValueFloat = maxValue, + ValueStep = valueStep, + ToolTip = tooltip + }; + numberInput.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); + + numberInput.OnValueChanged += _ => onChanged(); + + return new SettingValue( + () => numberInput.FloatValue, + i => numberInput.FloatValue = (float)Math.Round(i, 1)); + } + + static SettingValue CreateSelectionCarousel( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + SettingCarouselElement defaultValue, + float verticalSize, + ImmutableArray> options, + Action onChanged) + { + GUILayoutGroup inputContainer = CreateSettingBase( + parent, + description, + tooltip, + horizontalSize: 0.55f, + verticalSize: verticalSize); + + GUIButton minusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIButtonToggleLeft", + textAlignment: Alignment.Center) + { + UserData = -1 + }; + GUIFrame inputFrame = new GUIFrame( + new RectTransform(Vector2.One, inputContainer.RectTransform), + style: null); + GUINumberInput numberInput = new GUINumberInput( + new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), + NumberType.Int, + textAlignment: Alignment.Center, + style: "GUITextBox", + buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden) { IntValue = options.IndexOf(defaultValue), MinValueInt = 0, MaxValueInt = options.Length, - Visible = false + Visible = false, + ToolTip = tooltip }; inputContainer.RectTransform.Parent.MinSize = new Point(0, numberInput.RectTransform.MinSize.Y); - GUITextBox inputLabel = new GUITextBox(new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), text: defaultValue.Label.Value, textAlignment: Alignment.Center, createPenIcon: false) + GUITextBox inputLabel = new GUITextBox( + new RectTransform(Vector2.One, inputFrame.RectTransform, Anchor.Center), + text: defaultValue.Label.Value, + textAlignment: Alignment.Center, + createPenIcon: false) { CanBeFocused = false }; - GUIButton plusButton = new GUIButton(new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonToggleRight", textAlignment: Alignment.Center) { UserData = 1 }; + GUIButton plusButton = new GUIButton( + new RectTransform(Vector2.One, inputContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIButtonToggleRight", + textAlignment: Alignment.Center) + { + UserData = 1 + }; minusButton.OnClicked = plusButton.OnClicked = ChangeValue; @@ -381,20 +731,33 @@ namespace Barotrauma inputLabel.Text = options[value].Label.Value; } - return new SettingValue(() => options[numberInput.IntValue].Value, t => SetValue(options.IndexOf(e => Equals(e.Value, t)))); + return new SettingValue( + () => options[numberInput.IntValue].Value, + t => SetValue(options.IndexOf(e => Equals(e.Value, t))) + ); } - static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize, Action onChanged) + static SettingValue CreateTickbox( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + bool defaultValue, + float verticalSize, + Action onChanged) { - GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.7f, verticalSize); - GUILayoutGroup tickboxContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), inputContainer.RectTransform), childAnchor: Anchor.Center); - GUITickBox tickBox = new GUITickBox(new RectTransform(Vector2.One, tickboxContainer.RectTransform), string.Empty) + GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.625f, verticalSize); + GUILayoutGroup tickboxContainer = new GUILayoutGroup( + new RectTransform(new Vector2(0.375f, 1.0f), inputContainer.RectTransform), + childAnchor: Anchor.Center); + GUITickBox tickBox = new GUITickBox( + new RectTransform(Vector2.One, tickboxContainer.RectTransform), + string.Empty) { Selected = defaultValue, ToolTip = tooltip }; tickBox.Box.IgnoreLayoutGroups = true; - tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); + tickBox.Box.RectTransform.SetPosition(Anchor.CenterLeft); inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); tickBox.OnSelected += _ => @@ -406,11 +769,29 @@ namespace Barotrauma return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); } - static GUILayoutGroup CreateSettingBase(GUIComponent parent, LocalizedString description, LocalizedString tooltip, float horizontalSize, float verticalSize) + static GUILayoutGroup CreateSettingBase( + GUIComponent parent, + LocalizedString description, + LocalizedString tooltip, + float horizontalSize, + float verticalSize) { - GUILayoutGroup settingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(horizontalSize, 1f), settingHolder.RectTransform), description, font: parent.Rect.Width < 320 ? GUIStyle.SmallFont : GUIStyle.Font, wrap: true) { ToolTip = tooltip }; - GUILayoutGroup inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f - horizontalSize, 0.8f), settingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + GUILayoutGroup settingHolder = new GUILayoutGroup( + new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft); + GUITextBlock descriptionBlock = new GUITextBlock( + new RectTransform(new Vector2(horizontalSize, 1f), settingHolder.RectTransform), + description, + font: parent.Rect.Width < 320 ? GUIStyle.SmallFont : GUIStyle.Font, + wrap: true) + { + ToolTip = tooltip + }; + GUILayoutGroup inputContainer = new GUILayoutGroup( + new RectTransform(new Vector2(1f - horizontalSize, 0.8f), settingHolder.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 0a4a9958e..4d153ba33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -1,10 +1,7 @@ -using Barotrauma.Extensions; -using Barotrauma.IO; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; namespace Barotrauma { @@ -14,16 +11,20 @@ namespace Barotrauma private int prevInitialMoney; + private CampaignSettingElements campaignSettingElements; + + public bool LoadGameMenuVisible => loadGameContainer is { Visible: true }; + public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, List saveFiles = null) : base(newGameContainer, loadGameContainer) { var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: false) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.025f }; - GUILayoutGroup nameSeedLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), verticalLayout.RectTransform), isHorizontal: false) + GUILayoutGroup nameSeedLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), verticalLayout.RectTransform), isHorizontal: false) { Stretch = true }; @@ -31,119 +32,41 @@ namespace Barotrauma GUILayoutGroup campaignSettingLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), verticalLayout.RectTransform), isHorizontal: false) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.0f }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), string.Empty) + var saveLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("SaveName"), textAlignment: Alignment.CenterLeft); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), saveLabel.RectTransform, Anchor.CenterRight), string.Empty) { textFilterFunction = ToolBox.RemoveInvalidFileNameChars }; + saveLabel.InheritTotalChildrenMinHeight(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), ToolBox.RandomSeed(8)); + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("MapSeed"), textAlignment: Alignment.CenterLeft); + seedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight), ToolBox.RandomSeed(8)); + seedLabel.InheritTotalChildrenMinHeight(); - nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); + nameSeedLayout.InheritTotalChildrenMinHeight(); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty, false); + campaignSettingElements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty, false); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), - verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); + verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(30)) }, childAnchor: Anchor.BottomRight, isHorizontal: true); - StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) - { - OnClicked = (GUIButton btn, object userData) => - { - if (string.IsNullOrWhiteSpace(saveNameBox.Text)) - { - saveNameBox.Flash(GUIStyle.Red); - return false; - } - - SubmarineInfo selectedSub = null; - - if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } - selectedSub = GameMain.NetLobbyScreen.SelectedSub; - - if (selectedSub.SubmarineClass == SubmarineClass.Undefined) - { - new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); - return false; - } - - if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) - { - new GUIMessageBox(TextManager.Get("error"), TextManager.Get("nohashsubmarineselected")); - return false; - } - - string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); - bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - - CampaignSettings settings = elements.CreateSettings(); - - if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) - { - if (!hasRequiredContentPackages) - { - var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), - TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - msgBox.Buttons[0].OnClicked = msgBox.Close; - msgBox.Buttons[0].OnClicked += (button, obj) => - { - if (GUIMessageBox.MessageBoxes.Count == 0) - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } - return true; - }; - - msgBox.Buttons[1].OnClicked = msgBox.Close; - } - - if (selectedSub.HasTag(SubmarineTag.Shuttle)) - { - var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), - TextManager.Get("ShuttleWarning"), - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - msgBox.Buttons[0].OnClicked = (button, obj) => - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - } - else - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } - - return true; - } - }; - StartButton.RectTransform.MaxSize = RectTransform.MaxPoint; - StartButton.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); - - prevInitialMoney = 8000; - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) + prevInitialMoney = CampaignSettings.DefaultInitialMoney; + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green, textAlignment: Alignment.CenterRight) { TextGetter = () => { - int initialMoney = 8000; - if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + int defaultInitialMoney = CampaignSettings.DefaultInitialMoney; + int initialMoney = defaultInitialMoney; + if (CampaignModePresets.TryGetAttribute( + nameof(CampaignSettings.StartingBalanceAmount).ToIdentifier(), + campaignSettingElements.StartingFunds.GetValue().ToIdentifier(), + out var attribute)) { - initialMoney = definition.GetInt(elements.StartingFunds.GetValue().ToIdentifier()); + initialMoney = attribute.GetAttributeInt(defaultInitialMoney); } if (prevInitialMoney != initialMoney) { @@ -165,6 +88,87 @@ namespace Barotrauma CreateLoadMenu(saveFiles); } + public bool StartGameClicked(GUIButton button, object userdata) + { + if (string.IsNullOrWhiteSpace(saveNameBox.Text)) + { + saveNameBox.Flash(GUIStyle.Red, flashDuration: 5.0f); + saveNameBox.Pulsate(Vector2.One, Vector2.One * 1.2f, duration: 2.0f); + newGameContainer?.Flash(GUIStyle.Red, flashDuration: 0.5f); + return false; + } + + SubmarineInfo selectedSub = null; + + if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } + selectedSub = GameMain.NetLobbyScreen.SelectedSub; + + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); + return false; + } + + if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("nohashsubmarineselected")); + return false; + } + + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); + bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; + + CampaignSettings settings = campaignSettingElements.CreateSettings(); + + if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) + { + if (!hasRequiredContentPackages) + { + var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), + TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = msgBox.Close; + msgBox.Buttons[0].OnClicked += (button, obj) => + { + if (GUIMessageBox.MessageBoxes.Count == 0) + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + } + return true; + }; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + if (selectedSub.HasTag(SubmarineTag.Shuttle)) + { + var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), + TextManager.Get("ShuttleWarning"), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = (button, obj) => + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + return false; + } + } + else + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + } + + return true; + } + private IEnumerable WaitForCampaignSetup() { GUI.SetCursorWaiting(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 7dc80f765..9c1871345 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -127,7 +127,7 @@ namespace Barotrauma.CharacterEditor { Submarine.MainSub.PhysicsBody.Enabled = false; } - originalWall = new WallGroup(new List(Structure.WallList)); + wallGroups[0] = new WallGroup(new List(MapEntity.MapEntityList)); CloneWalls(); CalculateMovementLimits(); isEndlessRunner = true; @@ -554,8 +554,8 @@ namespace Barotrauma.CharacterEditor selectedJoints.Clear(); foreach (var w in jointSelectionWidgets.Values) { - w.refresh(); - w.linkedWidget?.refresh(); + w.Refresh(); + w.LinkedWidget?.Refresh(); } reset = true; } @@ -677,8 +677,8 @@ namespace Barotrauma.CharacterEditor } character.ControlLocalPlayer((float)deltaTime, Cam, false); character.Control((float)deltaTime, Cam); - character.AnimController.UpdateAnim((float)deltaTime); - character.AnimController.Update((float)deltaTime, Cam); + character.AnimController.UpdateAnimations((float)deltaTime); + character.AnimController.UpdateRagdoll((float)deltaTime, Cam); character.CurrentHull = character.AnimController.CurrentHull; if (isEndlessRunner) { @@ -722,7 +722,7 @@ namespace Barotrauma.CharacterEditor limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime)); animationWidgets.Values.ForEach(w => w.Update((float)deltaTime)); // Handle limb selection - if (PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == null && Widget.selectedWidgets.None()) + if (PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == null && Widget.SelectedWidgets.None()) { foreach (Limb limb in character.AnimController.Limbs) { @@ -779,7 +779,7 @@ namespace Barotrauma.CharacterEditor Submarine.MainSub?.UpdateTransform(); // Lightmaps - if (GameMain.LightManager.LightingEnabled) + if (GameMain.LightManager.LightingEnabled && Character.Controlled != null) { GameMain.LightManager.ObstructVision = Character.Controlled.ObstructVision; GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); @@ -791,7 +791,8 @@ namespace Barotrauma.CharacterEditor // Submarine spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: Cam.Transform); - Submarine.Draw(spriteBatch, isEndlessRunner); + Submarine.DrawBack(spriteBatch, editing: isEndlessRunner); + Submarine.DrawFront(spriteBatch, editing: isEndlessRunner); spriteBatch.End(); // Character(s) @@ -831,7 +832,7 @@ namespace Barotrauma.CharacterEditor { if (selectedLimbs.Contains(limb) || selectedLimbs.None()) { - limb.DrawDamageModifiers(spriteBatch, cam, SimToScreen(limb.SimPosition), isScreenSpace: true); + limb.DrawDamageModifiers(spriteBatch, cam, cam.WorldToScreen(limb.DrawPosition), isScreenSpace: true); } } } @@ -902,8 +903,8 @@ namespace Barotrauma.CharacterEditor if (jointStartLimb != null) { // TODO: there's something wrong here - var offset = anchor1Pos.HasValue ? Vector2.Transform(ConvertUnits.ToSimUnits(anchor1Pos.Value), Matrix.CreateRotationZ(jointStartLimb.Rotation)) : Vector2.Zero; - var startPos = SimToScreen(jointStartLimb.SimPosition + offset); + var offset = anchor1Pos.HasValue ? Vector2.Transform(anchor1Pos.Value, Matrix.CreateRotationZ(jointStartLimb.Rotation)) : Vector2.Zero; + var startPos = cam.WorldToScreen(jointStartLimb.DrawPosition + offset); GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3); } } @@ -915,8 +916,7 @@ namespace Barotrauma.CharacterEditor } if (isEndlessRunner) { - Structure wall = CurrentWall.walls.FirstOrDefault(); - Vector2 indicatorPos = wall == null ? originalWall.walls.First().DrawPosition : wall.DrawPosition; + Vector2 indicatorPos = MiddleWall.Entities.First().DrawPosition; GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.White); } GUI.Draw(Cam, spriteBatch); @@ -951,9 +951,10 @@ namespace Barotrauma.CharacterEditor { var topLeft = spriteSheetControls.RectTransform.TopLeft; bool useSpritesheetOrientation = float.IsNaN(lastLimb.Params.SpriteOrientation); - GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation") + ":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUIStyle.Font); + GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation")+":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUIStyle.Font); float orientation = useSpritesheetOrientation ? RagdollParams.SpritesheetOrientation : lastLimb.Params.SpriteOrientation; - DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), orientation, string.Empty, useSpritesheetOrientation ? Color.White : Color.Yellow, + DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), orientation, + GetCharacterEditorTranslation("spriteorientationtooltip") + "\n\n" + GetCharacterEditorTranslation("generalorientationtooltip"), useSpritesheetOrientation ? Color.White : Color.Yellow, angle => { TryUpdateSubParam(lastLimb.Params, "spriteorientation".ToIdentifier(), angle); @@ -968,7 +969,8 @@ namespace Barotrauma.CharacterEditor { var topLeft = spriteSheetControls.RectTransform.TopLeft; GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUIStyle.Font); - DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, + DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, + GetCharacterEditorTranslation("spritesheetorientationtooltip") + "\n\n" + GetCharacterEditorTranslation("generalorientationtooltip"), Color.White, angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } } @@ -1342,92 +1344,61 @@ namespace Barotrauma.CharacterEditor private int max; private void CalculateMovementLimits() { - min = CurrentWall.walls.Select(w => w.Rect.Left).OrderBy(p => p).First(); - max = CurrentWall.walls.Select(w => w.Rect.Right).OrderBy(p => p).Last(); + min = MiddleWall.Entities.Select(w => w.Rect.Left).OrderBy(p => p).First(); + max = MiddleWall.Entities.Select(w => w.Rect.Right).OrderBy(p => p).Last(); } - private WallGroup originalWall; - private WallGroup[] clones = new WallGroup[3]; - private IEnumerable AllWalls => originalWall.walls.Concat(clones.SelectMany(c => c.walls)); + private readonly WallGroup[] wallGroups = new WallGroup[3]; - private WallGroup _currentWall; - private WallGroup CurrentWall - { - get - { - if (_currentWall == null) - { - _currentWall = originalWall; - } - return _currentWall; - } - set - { - _currentWall = value; - } - } + private WallGroup MiddleWall => wallGroups[1]; + + private IEnumerable AllStructures => wallGroups.SelectMany(c => c.Entities); private class WallGroup { - public readonly List walls; + public readonly List Entities; - public WallGroup(List walls) + public WallGroup(List entities) { - this.walls = walls; + Entities = entities; } public WallGroup Clone() { - var clones = new List(); - walls.ForEachMod(w => clones.Add(w.Clone() as Structure)); + var clones = new List(); + Entities.ForEachMod(w => clones.Add(w.Clone())); return new WallGroup(clones); } } private void CloneWalls() { - for (int i = 0; i < 3; i++) + var originalWall = wallGroups[0]; + int moveAmount = originalWall.Entities.FirstOrDefault(e => e is Structure).Rect.Width; + for (int i = 1; i <= 2; i++) { - clones[i] = originalWall.Clone(); - for (int j = 0; j < originalWall.walls.Count; j++) + wallGroups[i] = originalWall.Clone(); + foreach (var entity in wallGroups[i].Entities) { - if (i == 1) - { - clones[i].walls[j].Move(new Vector2(originalWall.walls[j].Rect.Width, 0)); - } - else if (i == 2) - { - clones[i].walls[j].Move(new Vector2(-originalWall.walls[j].Rect.Width, 0)); - } + entity.Move(new Vector2(moveAmount * i, 0)); } } } - private WallGroup SelectClosestWallGroup(Vector2 pos) - { - var closestWall = clones.SelectMany(c => c.walls).OrderBy(w => Vector2.Distance(pos, w.Position)).First(); - return clones.Where(c => c.walls.Contains(closestWall)).FirstOrDefault(); - } - - private WallGroup SelectLastClone(bool right) - { - var lastWall = right - ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() - : clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Left).First(); - return clones.Where(c => c.walls.Contains(lastWall)).FirstOrDefault(); - } - private void UpdateWalls(bool right) { - CurrentWall = SelectClosestWallGroup(character.Position); - CalculateMovementLimits(); - var lastClone = SelectLastClone(!right); - for (int i = 0; i < lastClone.walls.Count; i++) + int moveAmount = wallGroups[0].Entities.FirstOrDefault(e => e is Structure).Rect.Width; + int amount = right ? moveAmount : -moveAmount; + foreach (var wallGroup in wallGroups) { - var amount = right ? lastClone.walls[i].Rect.Width : -lastClone.walls[i].Rect.Width; - var distance = CurrentWall.walls[i].Position.X - lastClone.walls[i].Position.X; - lastClone.walls[i].Move(new Vector2(amount + distance, 0)); + foreach (var entity in wallGroup.Entities) + { + entity.Move(new Vector2(amount, 0)); + } } + + CalculateMovementLimits(); + GameMain.World.ProcessChanges(); } @@ -1437,7 +1408,7 @@ namespace Barotrauma.CharacterEditor if (!isEndlessRunner) { return; } wallCollisionsEnabled = enabled; var collisionCategory = enabled ? FarseerPhysics.Dynamics.Category.Cat1 : FarseerPhysics.Dynamics.Category.None; - AllWalls.ForEach(w => w.SetCollisionCategory(collisionCategory)); + AllStructures.ForEach(w => (w as Structure)?.SetCollisionCategory(collisionCategory)); GameMain.World.ProcessChanges(); } #endregion @@ -1606,7 +1577,7 @@ namespace Barotrauma.CharacterEditor private void ClearWidgets() { - Widget.selectedWidgets.Clear(); + Widget.SelectedWidgets.Clear(); animationWidgets.Clear(); jointSelectionWidgets.Clear(); limbEditWidgets.Clear(); @@ -1618,8 +1589,8 @@ namespace Barotrauma.CharacterEditor selectedJoints.Clear(); foreach (var w in jointSelectionWidgets.Values) { - w.refresh(); - w.linkedWidget?.refresh(); + w.Refresh(); + w.LinkedWidget?.Refresh(); } } @@ -1666,6 +1637,11 @@ namespace Barotrauma.CharacterEditor public bool CreateCharacter(Identifier name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable animations = null) { + if (name.IsEmpty) + { + throw new ArgumentException("Name cannot be empty."); + } + var vanilla = GameMain.VanillaContent; if (contentPackage == null) @@ -2865,6 +2841,8 @@ namespace Barotrauma.CharacterEditor #endif character.AnimController.SaveRagdoll(inputField.Text); GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), Color.Green, font: GUIStyle.Font); + RagdollParams.ClearCache(); + ResetParamsEditor(); box.Close(); return true; }; @@ -2950,7 +2928,10 @@ namespace Barotrauma.CharacterEditor loadBox.Buttons[1].OnClicked += (btn, data) => { string fileName = Path.GetFileNameWithoutExtension(selectedFile); - var ragdoll = character.IsHumanoid ? HumanRagdollParams.GetRagdollParams(character.SpeciesName, fileName) as RagdollParams : RagdollParams.GetRagdollParams(character.SpeciesName, fileName); + Identifier baseSpecies = character.GetBaseCharacterSpeciesName(); + var ragdoll = character.IsHumanoid + ? RagdollParams.GetRagdollParams(character.SpeciesName, baseSpecies, fileName, character.Prefab.ContentPackage) as RagdollParams + : RagdollParams.GetRagdollParams(character.SpeciesName, baseSpecies, fileName, character.Prefab.ContentPackage); ragdoll.Reset(true); GUI.AddMessage(GetCharacterEditorTranslation("RagdollLoadedFrom").Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); RecreateRagdoll(ragdoll); @@ -3003,8 +2984,11 @@ namespace Barotrauma.CharacterEditor #endif var animParams = character.AnimController.GetAnimationParamsFromType(selectedType); if (animParams == null) { return true; } - animParams.Save(inputField.Text); - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", animParams.AnimationType.ToString()).Replace("[path]", animParams.Path.Value), Color.Green, font: GUIStyle.Font); + string fileName = inputField.Text; + animParams.Save(fileName); + string newPath = animParams.Path.ToString(); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", selectedType.ToString()).Replace("[path]", newPath), Color.Green, font: GUIStyle.Font); + AnimationParams.ClearCache(); ResetParamsEditor(); box.Close(); return true; @@ -3047,7 +3031,7 @@ namespace Barotrauma.CharacterEditor { listBox.ClearChildren(); var filePaths = Directory.GetFiles(CurrentAnimation.Folder); - foreach (var path in AnimationParams.FilterFilesByType(filePaths, selectedType)) + foreach (var path in AnimationParams.FilterAndSortFiles(filePaths, selectedType)) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80)) { @@ -3068,7 +3052,7 @@ namespace Barotrauma.CharacterEditor { selectedFile = data as string; // Don't allow to delete the animation that is currently in use, nor the default file. - var fileName = Path.GetFileNameWithoutExtension(selectedFile); + string fileName = Path.GetFileNameWithoutExtension(selectedFile); deleteButton.Enabled = fileName != CurrentAnimation.Name && fileName != AnimationParams.GetDefaultFileName(character.SpeciesName, CurrentAnimation.AnimationType); return true; }; @@ -3108,54 +3092,11 @@ namespace Barotrauma.CharacterEditor }; loadBox.Buttons[1].OnClicked += (btn, data) => { - string fileName = Path.GetFileNameWithoutExtension(selectedFile); - if (character.IsHumanoid && character.AnimController is HumanoidAnimController humanAnimController) + if (character.AnimController.TryLoadAnimation(selectedType, Path.GetFileNameWithoutExtension(selectedFile), out AnimationParams animationParams, throwErrors: true)) { - switch (selectedType) - { - case AnimationType.Walk: - humanAnimController.WalkParams = HumanWalkParams.GetAnimParams(character, fileName); - break; - case AnimationType.Run: - humanAnimController.RunParams = HumanRunParams.GetAnimParams(character, fileName); - break; - case AnimationType.Crouch: - humanAnimController.HumanCrouchParams = HumanCrouchParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimSlow: - humanAnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimFast: - humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, fileName); - break; - default: - DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); - break; - } + animationParams.Reset(forceReload: true); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", animationParams.FileNameWithoutExtension), Color.WhiteSmoke, font: GUIStyle.Font); } - else - { - switch (selectedType) - { - case AnimationType.Walk: - character.AnimController.WalkParams = FishWalkParams.GetAnimParams(character, fileName); - break; - case AnimationType.Run: - character.AnimController.RunParams = FishRunParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimSlow: - character.AnimController.SwimSlowParams = FishSwimSlowParams.GetAnimParams(character, fileName); - break; - case AnimationType.SwimFast: - character.AnimController.SwimFastParams = FishSwimFastParams.GetAnimParams(character, fileName); - break; - default: - DebugConsole.ThrowErrorLocalized(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); - break; - } - } - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); - character.AnimController.AllAnimParams.ForEach(a => a.Reset(forceReload: true)); ResetParamsEditor(); loadBox.Close(); return true; @@ -3810,7 +3751,7 @@ namespace Barotrauma.CharacterEditor private void DrawAnimationControls(SpriteBatch spriteBatch, float deltaTime) { var collider = character.AnimController.Collider; - var colliderDrawPos = SimToScreen(collider.SimPosition); + var colliderDrawPos = cam.WorldToScreen(collider.DrawPosition); var animParams = character.AnimController.CurrentAnimationParams; var groundedParams = animParams as GroundedMovementParams; var humanParams = animParams as IHumanAnimation; @@ -3837,20 +3778,20 @@ namespace Barotrauma.CharacterEditor GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 120, 150), GetCharacterEditorTranslation("HoldLeftAltToAdjustCycleSpeed"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font); } // Widgets for all anims --> - Vector2 referencePoint = SimToScreen(head != null ? head.SimPosition: collider.SimPosition); + Vector2 referencePoint = cam.WorldToScreen(head != null ? head.DrawPosition: collider.DrawPosition); Vector2 drawPos = referencePoint; if (ShowCycleWidget()) { - GetAnimationWidget("CycleSpeed", Color.MediumPurple, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => + GetAnimationWidget("CycleSpeed", Color.MediumPurple, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: WidgetShape.Circle, initMethod: w => { float multiplier = 0.5f; - w.tooltip = GetCharacterEditorTranslation("CycleSpeed"); - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("CycleSpeed"); + w.Refresh = () => { - var refPoint = SimToScreen(head != null ? head.SimPosition : collider.SimPosition); + var refPoint = cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition); w.DrawPos = refPoint + GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(CurrentAnimation.CycleSpeed * multiplier) * Cam.Zoom; // Update tooltip, because the cycle speed might be automatically adjusted by the movement speed widget. - w.tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; }; w.MouseHeld += dTime => { @@ -3859,7 +3800,7 @@ namespace Barotrauma.CharacterEditor //w.DrawPos = newPos; float speed = CurrentAnimation.CycleSpeed + ConvertUnits.ToSimUnits(Vector2.Multiply(PlayerInput.MouseSpeed / multiplier, GetScreenSpaceForward()).Combine()) / Cam.Zoom; TryUpdateAnimParam("cyclespeed", speed); - w.tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("CycleSpeed")}: {CurrentAnimation.CycleSpeed.FormatDoubleDecimal()}"; }; // Additional check, which overrides the previous value (because evaluated last) w.PreUpdate += dTime => @@ -3874,27 +3815,27 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head != null ? head.SimPosition : collider.SimPosition), Color.MediumPurple); + GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition), Color.MediumPurple); } }; }).Draw(spriteBatch, deltaTime); } else { - GetAnimationWidget("MovementSpeed", Color.Turquoise, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: Widget.Shape.Circle, initMethod: w => + GetAnimationWidget("MovementSpeed", Color.Turquoise, Color.Black, size: 20, sizeMultiplier: 1.5f, shape: WidgetShape.Circle, initMethod: w => { float multiplier = 0.5f; - w.tooltip = GetCharacterEditorTranslation("MovementSpeed"); - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("MovementSpeed"); + w.Refresh = () => { - var refPoint = SimToScreen(head != null ? head.SimPosition : collider.SimPosition); + var refPoint = cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition); w.DrawPos = refPoint + GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(CurrentAnimation.MovementSpeed * multiplier) * Cam.Zoom; }; w.MouseHeld += dTime => @@ -3909,7 +3850,7 @@ namespace Barotrauma.CharacterEditor { TryUpdateAnimParam("cyclespeed", character.AnimController.CurrentAnimationParams.MovementSpeed); } - w.tooltip = $"{GetCharacterEditorTranslation("MovementSpeed")}: {CurrentAnimation.MovementSpeed.FormatSingleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("MovementSpeed")}: {CurrentAnimation.MovementSpeed.FormatSingleDecimal()}"; }; // Additional check, which overrides the previous value (because evaluated last) w.PreUpdate += dTime => @@ -3924,14 +3865,14 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head != null ? head.SimPosition : collider.SimPosition), Color.Turquoise); + GUI.DrawLine(spriteBatch, w.DrawPos, Cam.WorldToScreen(head != null ? head.DrawPosition : collider.DrawPosition), Color.Turquoise); } }; }).Draw(spriteBatch, deltaTime); @@ -3940,7 +3881,7 @@ namespace Barotrauma.CharacterEditor if (head != null) { // Head angle - DrawRadialWidget(spriteBatch, SimToScreen(head.SimPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, + DrawRadialWidget(spriteBatch, Cam.WorldToScreen(head.DrawPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + head.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); // Head position and leaning Color color = GUIStyle.Red; @@ -3950,8 +3891,11 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("Head"); - w.refresh = () => w.DrawPos = SimToScreen(head.SimPosition.X + humanAnimController.HeadLeanAmount * character.AnimController.Dir, head.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("Head"); + w.Refresh = () => w.DrawPos = Cam.WorldToScreen( + new Vector2( + head.DrawPosition.X + ConvertUnits.ToDisplayUnits(humanAnimController.HeadLeanAmount * character.AnimController.Dir), + ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y))); bool isHorizontal = false; bool isDirectionSet = false; w.MouseDown += () => isDirectionSet = false; @@ -3969,23 +3913,23 @@ namespace Barotrauma.CharacterEditor if (isHorizontal) { TryUpdateAnimParam("headleanamount", humanGroundedParams.HeadLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); } else { TryUpdateAnimParam("headposition", humanGroundedParams.HeadPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } } else { TryUpdateAnimParam("headleanamount", humanGroundedParams.HeadLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); TryUpdateAnimParam("headposition", humanGroundedParams.HeadPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } }; @@ -4012,7 +3956,7 @@ namespace Barotrauma.CharacterEditor } else if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(head.SimPosition), color); + GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(head.DrawPosition), color); } }; }).Draw(spriteBatch, deltaTime); @@ -4021,11 +3965,11 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("HeadPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("HeadPosition"); - w.refresh = () => w.DrawPos = SimToScreen(head.SimPosition.X, head.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("HeadPosition"); + w.Refresh = () => w.DrawPos = cam.WorldToScreen(new Vector2(head.DrawPosition.X, ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y))); w.MouseHeld += dTime => { - w.DrawPos = SimToScreen(head.SimPosition.X, head.PullJointWorldAnchorB.Y); + w.DrawPos = cam.WorldToScreen(new Vector2(head.DrawPosition.X, ConvertUnits.ToDisplayUnits(head.PullJointWorldAnchorB.Y))); var scaledInput = ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed) / Cam.Zoom / RagdollParams.JointScale; TryUpdateAnimParam("headposition", groundedParams.HeadPosition - scaledInput.Y); }; @@ -4042,14 +3986,14 @@ namespace Barotrauma.CharacterEditor } if (torso != null) { - referencePoint = torso.SimPosition; + referencePoint = torso.DrawPosition; if (animParams is HumanGroundedParams || animParams is HumanSwimParams) { var f = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); - referencePoint -= f * 0.25f; + referencePoint -= f * 25f; } // Torso angle - DrawRadialWidget(spriteBatch, SimToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White, + DrawRadialWidget(spriteBatch, cam.WorldToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White, angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: -collider.Rotation + torso.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); Color color = Color.DodgerBlue; if (animParams.IsGroundedAnimation) @@ -4059,8 +4003,10 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("Torso"); - w.refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X + humanAnimController.TorsoLeanAmount * character.AnimController.Dir, torso.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("Torso"); + w.Refresh = () => w.DrawPos = cam.WorldToScreen( + new Vector2(torso.DrawPosition.X + ConvertUnits.ToDisplayUnits(humanAnimController.TorsoLeanAmount * character.AnimController.Dir), + ConvertUnits.ToDisplayUnits(torso.PullJointWorldAnchorB.Y))); bool isHorizontal = false; bool isDirectionSet = false; w.MouseDown += () => isDirectionSet = false; @@ -4078,23 +4024,23 @@ namespace Barotrauma.CharacterEditor if (isHorizontal) { TryUpdateAnimParam("torsoleanamount", humanGroundedParams.TorsoLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); } else { TryUpdateAnimParam("torsoposition", humanGroundedParams.TorsoPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } } else { TryUpdateAnimParam("torsoleanamount", humanGroundedParams.TorsoLeanAmount + scaledInput.X * character.AnimController.Dir); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(PlayerInput.MousePosition.X, w.DrawPos.Y); TryUpdateAnimParam("torsoposition", humanGroundedParams.TorsoPosition - scaledInput.Y / RagdollParams.JointScale); - w.refresh(); + w.Refresh(); w.DrawPos = new Vector2(w.DrawPos.X, PlayerInput.MousePosition.Y); } }; @@ -4121,7 +4067,7 @@ namespace Barotrauma.CharacterEditor } else if (w.IsSelected) { - GUI.DrawLine(spriteBatch, w.DrawPos, SimToScreen(torso.SimPosition), color); + GUI.DrawLine(spriteBatch, w.DrawPos, cam.WorldToScreen(torso.DrawPosition), color); } }; }).Draw(spriteBatch, deltaTime); @@ -4130,8 +4076,8 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("TorsoPosition", color, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("TorsoPosition"); - w.refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y); + w.Tooltip = GetCharacterEditorTranslation("TorsoPosition"); + w.Refresh = () => w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y); w.MouseHeld += dTime => { w.DrawPos = SimToScreen(torso.SimPosition.X, torso.PullJointWorldAnchorB.Y); @@ -4152,7 +4098,7 @@ namespace Barotrauma.CharacterEditor // Tail angle if (tail != null && fishParams != null) { - DrawRadialWidget(spriteBatch, SimToScreen(tail.SimPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White, + DrawRadialWidget(spriteBatch, cam.WorldToScreen(tail.DrawPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White, angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + tail.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); } // Foot angle @@ -4171,7 +4117,7 @@ namespace Barotrauma.CharacterEditor } DrawRadialWidget(spriteBatch, - SimToScreen(new Vector2(limb.SimPosition.X, colliderBottom.Y)), + cam.WorldToScreen(new Vector2(limb.DrawPosition.X, ConvertUnits.ToDisplayUnits(colliderBottom.Y))), MathHelper.ToDegrees(fishParams.FootAnglesInRadians[limb.Params.ID]), GetCharacterEditorTranslation("FootAngle"), Color.White, angle => @@ -4184,7 +4130,7 @@ namespace Barotrauma.CharacterEditor } else if (humanParams != null) { - DrawRadialWidget(spriteBatch, SimToScreen(foot.SimPosition), humanParams.FootAngle, GetCharacterEditorTranslation("FootAngle"), Color.White, + DrawRadialWidget(spriteBatch, cam.WorldToScreen(foot.DrawPosition), humanParams.FootAngle, GetCharacterEditorTranslation("FootAngle"), Color.White, angle => TryUpdateAnimParam("footangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + foot.Params.GetSpriteOrientation() * dir, clockWise: dir > 0, wrapAnglePi: true); } // Grounded only @@ -4192,10 +4138,12 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("StepSize", Color.LimeGreen, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("StepSize"); - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("StepSize"); + w.Refresh = () => { - var refPoint = SimToScreen(character.AnimController.GetColliderBottom()); + var refPoint = cam.WorldToScreen(new Vector2( + character.AnimController.Collider.DrawPosition.X, + character.AnimController.GetColliderBottom().Y)); var stepSize = ConvertUnits.ToDisplayUnits(character.AnimController.StepSize.Value); w.DrawPos = refPoint + new Vector2(stepSize.X * character.AnimController.Dir, -stepSize.Y) * Cam.Zoom; }; @@ -4204,7 +4152,7 @@ namespace Barotrauma.CharacterEditor w.DrawPos = PlayerInput.MousePosition; var transformedInput = ConvertUnits.ToSimUnits(new Vector2(PlayerInput.MouseSpeed.X * character.AnimController.Dir, -PlayerInput.MouseSpeed.Y)) / Cam.Zoom / RagdollParams.JointScale; TryUpdateAnimParam("stepsize", groundedParams.StepSize + transformedInput); - w.tooltip = $"{GetCharacterEditorTranslation("StepSize")}: {groundedParams.StepSize.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("StepSize")}: {groundedParams.StepSize.FormatDoubleDecimal()}"; }; w.PostDraw += (sp, dTime) => { @@ -4223,11 +4171,11 @@ namespace Barotrauma.CharacterEditor { GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); - float offset = 0.1f; - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("HandMoveAmount"); + float offset = 10f; + w.Refresh = () => { - var refPoint = SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset); + var refPoint = cam.WorldToScreen(character.AnimController.Collider.DrawPosition + GetSimSpaceForward() * offset); var handMovement = ConvertUnits.ToDisplayUnits(humanGroundedParams.HandMoveAmount); w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom; }; @@ -4236,13 +4184,13 @@ namespace Barotrauma.CharacterEditor w.DrawPos = PlayerInput.MousePosition; var transformedInput = ConvertUnits.ToSimUnits(new Vector2(PlayerInput.MouseSpeed.X * character.AnimController.Dir, PlayerInput.MouseSpeed.Y) / Cam.Zoom); TryUpdateAnimParam("handmoveamount", humanGroundedParams.HandMoveAmount + transformedInput); - w.tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanGroundedParams.HandMoveAmount.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanGroundedParams.HandMoveAmount.FormatDoubleDecimal()}"; }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); + GUI.DrawLine(sp, w.DrawPos, cam.WorldToScreen(character.AnimController.Collider.DrawPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4256,15 +4204,15 @@ namespace Barotrauma.CharacterEditor int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveAmplitude) * Cam.Zoom / amplitudeMultiplier; float GetWaveLength() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveLength) * Cam.Zoom / lengthMultiplier; - Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom; + Vector2 GetRefPoint() => cam.WorldToScreen(collider.DrawPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom; Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength(); Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude(); - var lengthWidget = GetAnimationWidget("WaveLength", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var lengthWidget = GetAnimationWidget("WaveLength", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("TailMovementSpeed"); - w.refresh = () => w.DrawPos = GetDrawPos(); + w.Tooltip = GetCharacterEditorTranslation("TailMovementSpeed"); + w.Refresh = () => w.DrawPos = GetDrawPos(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward()).Combine() / Cam.Zoom * lengthMultiplier; @@ -4275,14 +4223,14 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); - var amplitudeWidget = GetAnimationWidget("WaveAmplitude", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var amplitudeWidget = GetAnimationWidget("WaveAmplitude", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("TailMovementAmount"); - w.refresh = () => w.DrawPos = GetControlPoint(); + w.Tooltip = GetCharacterEditorTranslation("TailMovementAmount"); + w.Refresh = () => w.DrawPos = GetControlPoint(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward().Right()).Combine() * character.AnimController.Dir / Cam.Zoom * amplitudeMultiplier; @@ -4293,7 +4241,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); @@ -4313,15 +4261,15 @@ namespace Barotrauma.CharacterEditor int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(humanSwimParams.LegMoveAmount) * Cam.Zoom / amplitudeMultiplier; float GetWaveLength() => ConvertUnits.ToDisplayUnits(humanSwimParams.LegCycleLength) * Cam.Zoom / lengthMultiplier; - Vector2 GetRefPoint() => SimToScreen(character.SimPosition - GetScreenSpaceForward() / 2); + Vector2 GetRefPoint() => cam.WorldToScreen(character.DrawPosition - GetScreenSpaceForward().FlipY() * 75); Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength(); Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; Vector2 GetControlPoint() => GetStartPoint() + GetScreenSpaceForward().Right() * character.AnimController.Dir * GetAmplitude(); - var lengthWidget = GetAnimationWidget("LegMovementSpeed", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var lengthWidget = GetAnimationWidget("LegMovementSpeed", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("LegMovementSpeed"); - w.refresh = () => w.DrawPos = GetDrawPos(); + w.Tooltip = GetCharacterEditorTranslation("LegMovementSpeed"); + w.Refresh = () => w.DrawPos = GetDrawPos(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward()).Combine() / Cam.Zoom * lengthMultiplier; @@ -4332,14 +4280,14 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); - var amplitudeWidget = GetAnimationWidget("LegMovementAmount", Color.NavajoWhite, Color.Black, size: 15, shape: Widget.Shape.Circle, initMethod: w => + var amplitudeWidget = GetAnimationWidget("LegMovementAmount", Color.NavajoWhite, Color.Black, size: 15, shape: WidgetShape.Circle, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("LegMovementAmount"); - w.refresh = () => w.DrawPos = GetControlPoint(); + w.Tooltip = GetCharacterEditorTranslation("LegMovementAmount"); + w.Refresh = () => w.DrawPos = GetControlPoint(); w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward().Right()).Combine() * character.AnimController.Dir / Cam.Zoom * amplitudeMultiplier; @@ -4350,7 +4298,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsControlled) { - w.refresh(); + w.Refresh(); } }; }); @@ -4363,11 +4311,11 @@ namespace Barotrauma.CharacterEditor // Arms GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { - w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); - float offset = 0.4f; - w.refresh = () => + w.Tooltip = GetCharacterEditorTranslation("HandMoveAmount"); + float offset = 40f; + w.Refresh = () => { - var refPoint = SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset); + var refPoint = cam.WorldToScreen(collider.DrawPosition + GetSimSpaceForward() * offset); var handMovement = ConvertUnits.ToDisplayUnits(humanSwimParams.HandMoveAmount); w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom; }; @@ -4378,13 +4326,13 @@ namespace Barotrauma.CharacterEditor Vector2 handMovement = humanSwimParams.HandMoveAmount + transformedInput; TryUpdateAnimParam("handmoveamount", handMovement); TryUpdateAnimParam("handcyclespeed", handMovement.X * 4); - w.tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanSwimParams.HandMoveAmount.FormatDoubleDecimal()}"; + w.Tooltip = $"{GetCharacterEditorTranslation("HandMoveAmount")}: {humanSwimParams.HandMoveAmount.FormatDoubleDecimal()}"; }; w.PostDraw += (sp, dTime) => { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); + GUI.DrawLine(sp, w.DrawPos, cam.WorldToScreen(collider.DrawPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4407,7 +4355,7 @@ namespace Barotrauma.CharacterEditor { Vector2 size = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) * Cam.Zoom; Vector2 up = VectorExtensions.BackwardFlipped(limb.Rotation); - Vector2 limbScreenPos = SimToScreen(limb.SimPosition); + Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition); corners = MathUtils.GetImaginaryRect(corners, up, limbScreenPos, size); return corners; } @@ -4420,14 +4368,14 @@ namespace Barotrauma.CharacterEditor if (limb == null || limb.ActiveSprite == null) { continue; } var origin = limb.ActiveSprite.Origin; var sourceRect = limb.ActiveSprite.SourceRect; - Vector2 limbScreenPos = SimToScreen(limb.SimPosition); + Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition); bool isSelected = selectedLimbs.Contains(limb); corners = GetLimbPhysicRect(limb); if (isSelected && jointStartLimb != limb && jointEndLimb != limb) { GUI.DrawRectangle(spriteBatch, corners, Color.Yellow, thickness: 3); } - if (GUI.MouseOn == null && Widget.selectedWidgets.None() && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(corners, PlayerInput.MousePosition)) + if (GUI.MouseOn == null && Widget.SelectedWidgets.None() && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(corners, PlayerInput.MousePosition)) { if (isSelected) { @@ -4489,7 +4437,7 @@ namespace Barotrauma.CharacterEditor if (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot || limb.type == LimbType.LeftHand || limb.type == LimbType.RightHand) { var pullJointWidgetSize = new Vector2(5, 5); - Vector2 tformedPullPos = SimToScreen(limb.PullJointWorldAnchorA); + Vector2 tformedPullPos = SimToScreen(limb.PullJointWorldAnchorA) + limb.body.DrawPositionOffset; GUI.DrawRectangle(spriteBatch, tformedPullPos - pullJointWidgetSize / 2, pullJointWidgetSize, GUIStyle.Red, true); DrawWidget(spriteBatch, tformedPullPos, WidgetType.Rectangle, 8, Color.Cyan, $"IK ({limb.Name})", () => { @@ -4524,7 +4472,7 @@ namespace Barotrauma.CharacterEditor { continue; } - Vector2 limbScreenPos = SimToScreen(limb.SimPosition); + Vector2 limbScreenPos = cam.WorldToScreen(limb.DrawPosition); var f = Vector2.Transform(jointPos, Matrix.CreateRotationZ(limb.Rotation)); f.Y = -f.Y; Vector2 tformedJointPos = limbScreenPos + f * Cam.Zoom; @@ -4560,16 +4508,19 @@ namespace Barotrauma.CharacterEditor } DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: rotation, holdPosition: true); } + Limb referenceLimb = altDown ? joint.LimbB : joint.LimbA; // Is the direction inversed incorrectly? - Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation - joint.LimbB.Params.GetSpriteOrientation(), 20); - GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.Magenta, width: 2); + Vector2 to = tformedJointPos - VectorExtensions.ForwardFlipped(referenceLimb.Rotation - referenceLimb.Params.GetSpriteOrientation(), 150); + GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.LightGray * 0.7f, width: 2); var dotSize = new Vector2(5, 5); var rect = new Rectangle((tformedJointPos - dotSize / 2).ToPoint(), dotSize.ToPoint()); //GUI.DrawRectangle(spriteBatch, tformedJointPos - dotSize / 2, dotSize, color, true); //GUI.DrawLine(spriteBatch, tformedJointPos, tformedJointPos + up * 20, Color.White, width: 3); - GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow, width: 3); + //GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow * 0.5f, width: 3); //GUI.DrawRectangle(spriteBatch, inputRect, GUIStyle.Red); - GUI.DrawString(spriteBatch, tformedJointPos + new Vector2(dotSize.X, -dotSize.Y) * 2, $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}", Color.White, Color.Black * 0.5f); + + string tooltip = $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}"; + GUI.DrawString(spriteBatch, tformedJointPos - new Vector2(1.2f, 0.5f) * GUIStyle.Font.MeasureString(tooltip), tooltip, Color.White, Color.Black * 0.5f); if (PlayerInput.PrimaryMouseButtonHeld()) { if (!selectionWidget.IsControlled) { continue; } @@ -4843,10 +4794,10 @@ namespace Barotrauma.CharacterEditor Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2(); Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y); Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height); - var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb, widgetSize, Widget.Shape.Cross, initMethod: w => + var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb, widgetSize, WidgetShape.Cross, initMethod: w => { - w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Origin")}: {sprite.RelativeOrigin.FormatDoubleDecimal()}"; - w.refresh(); + w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Origin")}: {sprite.RelativeOrigin.FormatDoubleDecimal()}"; + w.Refresh(); w.MouseHeld += dTime => { var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite)); @@ -4883,16 +4834,16 @@ namespace Barotrauma.CharacterEditor var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite)); w.DrawPos = (spritePos + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * spriteSheetZoom) .Clamp(spritePos + GetTopLeft() * spriteSheetZoom, spritePos + GetBottomRight() * spriteSheetZoom); - w.refresh(); + w.Refresh(); }; }); originWidget.Draw(spriteBatch, deltaTime); if (!lockSpritePosition && (limb.type != LimbType.Head || !character.IsHuman)) { - var positionWidget = GetLimbEditWidget($"{limb.Params.ID}_position", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var positionWidget = GetLimbEditWidget($"{limb.Params.ID}_position", limb, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Position")}: {limb.ActiveSprite.SourceRect.Location}"; - w.refresh(); + w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Position")}: {limb.ActiveSprite.SourceRect.Location}"; + w.Refresh(); w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition; @@ -4924,7 +4875,7 @@ namespace Barotrauma.CharacterEditor }); }; }; - w.PreDraw += (sb, dTime) => w.refresh(); + w.PreDraw += (sb, dTime) => w.Refresh(); }); if (!positionWidget.IsControlled) { @@ -4934,10 +4885,10 @@ namespace Barotrauma.CharacterEditor } if (!lockSpriteSize && (limb.type != LimbType.Head || !character.IsHuman)) { - var sizeWidget = GetLimbEditWidget($"{limb.Params.ID}_size", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var sizeWidget = GetLimbEditWidget($"{limb.Params.ID}_size", limb, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.refresh = () => w.tooltip = $"{GetCharacterEditorTranslation("Size")}: {limb.ActiveSprite.SourceRect.Size}"; - w.refresh(); + w.Refresh = () => w.Tooltip = $"{GetCharacterEditorTranslation("Size")}: {limb.ActiveSprite.SourceRect.Size}"; + w.Refresh(); w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition; @@ -4979,7 +4930,7 @@ namespace Barotrauma.CharacterEditor }); }; }; - w.PreDraw += (sb, dTime) => w.refresh(); + w.PreDraw += (sb, dTime) => w.Refresh(); }); if (!sizeWidget.IsControlled) { @@ -4988,7 +4939,7 @@ namespace Barotrauma.CharacterEditor sizeWidget.Draw(spriteBatch, deltaTime); } } - else if (isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None()) + else if (isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None()) { // TODO: only one limb name should be displayed (needs to be done in a separate loop) GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f); @@ -4997,7 +4948,7 @@ namespace Barotrauma.CharacterEditor else { GUI.DrawRectangle(spriteBatch, rect, isMouseOn ? Color.White : Color.Gray); - if (isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None()) + if (isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None()) { // TODO: only one limb name should be displayed (needs to be done in a separate loop) GUI.DrawString(spriteBatch, limbScreenPos + new Vector2(10, -10), limb.Name, Color.White, Color.Black * 0.5f); @@ -5089,7 +5040,7 @@ namespace Barotrauma.CharacterEditor bool isHovered = jointSelectionWidget.IsSelected || otherWidget.IsSelected; if (isSelected || isHovered) { - GUI.DrawLine(spriteBatch, jointSelectionWidget.DrawPos, otherWidget.DrawPos, jointSelectionWidget.color, width: 2); + GUI.DrawLine(spriteBatch, jointSelectionWidget.DrawPos, otherWidget.DrawPos, jointSelectionWidget.Color, width: 2); } } if (selectedJoints.Contains(joint)) @@ -5369,14 +5320,14 @@ namespace Barotrauma.CharacterEditor GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUIStyle.SmallFont); } onClick(angle); - var zeroPos = drawPos + VectorExtensions.Forward(rotationOffset - MathHelper.PiOver2, circleRadius); - GUI.DrawLine(spriteBatch, drawPos, zeroPos, GUIStyle.Red, width: 3); }, autoFreeze, holdPosition, onHovered: () => { if (!PlayerInput.PrimaryMouseButtonHeld()) { - GUI.DrawString(spriteBatch, new Vector2(drawPos.X + 5, drawPos.Y - widgetSize / 2), - $"{toolTip} ({angle.FormatZeroDecimal()})", color, Color.Black * 0.5f); + GUIComponent.DrawToolTip( + spriteBatch, + $"{toolTip} ({angle.FormatZeroDecimal()})", + new Vector2(drawPos.X + 50, drawPos.Y - widgetSize / 2 - 50)); } }); } @@ -5388,7 +5339,7 @@ namespace Barotrauma.CharacterEditor var inputRect = drawRect; inputRect.Inflate(size * 0.75f, size * 0.75f); bool isMouseOn = inputRect.Contains(PlayerInput.MousePosition); - bool isSelected = isMouseOn && GUI.MouseOn == null && Widget.selectedWidgets.None(); + bool isSelected = isMouseOn && GUI.MouseOn == null && Widget.SelectedWidgets.None(); switch (widgetType) { case WidgetType.Rectangle: @@ -5420,7 +5371,7 @@ namespace Barotrauma.CharacterEditor // Label/tooltip if (onHovered == null) { - GUI.DrawString(spriteBatch, new Vector2(drawRect.Right + 5, drawRect.Y - drawRect.Height / 2), toolTip, color, Color.Black); + GUIComponent.DrawToolTip(spriteBatch, toolTip, new Vector2(drawRect.Right + 5, drawRect.Y - drawRect.Height / 2)); } else { @@ -5457,7 +5408,7 @@ namespace Barotrauma.CharacterEditor private Dictionary jointSelectionWidgets = new Dictionary(); private Dictionary limbEditWidgets = new Dictionary(); - private Widget GetAnimationWidget(string name, Color innerColor, Color? outerColor = null, int size = 10, float sizeMultiplier = 2, Widget.Shape shape = Widget.Shape.Rectangle, Action initMethod = null) + private Widget GetAnimationWidget(string name, Color innerColor, Color? outerColor = null, int size = 10, float sizeMultiplier = 2, WidgetShape shape = WidgetShape.Rectangle, Action initMethod = null) { string id = $"{character.SpeciesName}_{character.AnimController.CurrentAnimationParams.AnimationType.ToString()}_{name}"; if (!animationWidgets.TryGetValue(id, out Widget widget)) @@ -5465,32 +5416,32 @@ namespace Barotrauma.CharacterEditor int selectedSize = (int)Math.Round(size * sizeMultiplier); widget = new Widget(id, size, shape) { - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10), - data = character.AnimController.CurrentAnimationParams + TooltipOffset = new Vector2(selectedSize / 2 + 5, -10), + Data = character.AnimController.CurrentAnimationParams }; widget.MouseUp += () => CurrentAnimation.StoreSnapshot(); - widget.color = innerColor; - widget.secondaryColor = outerColor; + widget.Color = innerColor; + widget.SecondaryColor = outerColor; widget.PreUpdate += dTime => { widget.Enabled = editAnimations; if (widget.Enabled) { - AnimationParams data = widget.data as AnimationParams; + AnimationParams data = widget.Data as AnimationParams; widget.Enabled = data.AnimationType == character.AnimController.CurrentAnimationParams.AnimationType; } }; widget.PostUpdate += dTime => { - widget.inputAreaMargin = widget.IsControlled ? 1000 : 0; - widget.size = widget.IsSelected ? selectedSize : size; - widget.isFilled = widget.IsControlled; + widget.InputAreaMargin = widget.IsControlled ? 1000 : 0; + widget.Size = widget.IsSelected ? selectedSize : size; + widget.IsFilled = widget.IsControlled; }; widget.PreDraw += (sp, dTime) => { if (!widget.IsControlled) { - widget.refresh(); + widget.Refresh(); } }; animationWidgets.Add(id, widget); @@ -5511,8 +5462,8 @@ namespace Barotrauma.CharacterEditor { linkedWidget = CreateJointSelectionWidget(linkedId, joint); } - jointWidget.linkedWidget = linkedWidget; - linkedWidget.linkedWidget = jointWidget; + jointWidget.LinkedWidget = linkedWidget; + linkedWidget.LinkedWidget = jointWidget; } } return jointWidget; @@ -5522,21 +5473,18 @@ namespace Barotrauma.CharacterEditor { int normalSize = 10; int selectedSize = 20; - var widget = new Widget(ID, normalSize, Widget.Shape.Circle) + var widget = new Widget(ID, normalSize, WidgetShape.Circle); + widget.Refresh = () => { - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10) + widget.ShowTooltip = !selectedJoints.Contains(joint); + widget.Color = selectedJoints.Contains(joint) ? Color.Yellow : GUIStyle.Red; }; - widget.refresh = () => - { - widget.showTooltip = !selectedJoints.Contains(joint); - widget.color = selectedJoints.Contains(joint) ? Color.Yellow : GUIStyle.Red; - }; - widget.refresh(); + widget.Refresh(); widget.PreUpdate += dTime => widget.Enabled = editJoints; widget.PostUpdate += dTime => { - widget.inputAreaMargin = widget.IsControlled ? 1000 : 0; - widget.size = widget.IsSelected ? selectedSize : normalSize; + widget.InputAreaMargin = widget.IsControlled ? 1000 : 0; + widget.Size = widget.IsSelected ? selectedSize : normalSize; }; widget.MouseDown += () => { @@ -5555,8 +5503,8 @@ namespace Barotrauma.CharacterEditor } foreach (var w in jointSelectionWidgets.Values) { - w.refresh(); - w.linkedWidget?.refresh(); + w.Refresh(); + w.LinkedWidget?.Refresh(); } ResetParamsEditor(); }; @@ -5567,13 +5515,14 @@ namespace Barotrauma.CharacterEditor RagdollParams.StoreSnapshot(); } }; - widget.tooltip = joint.Params.Name; + widget.Tooltip = joint.Params.Name; + widget.TooltipOffset = new Vector2(-GUIStyle.Font.MeasureString(widget.Tooltip).X - 30, -10); jointSelectionWidgets.Add(ID, widget); return widget; } } - private Widget GetLimbEditWidget(string ID, Limb limb, int size = 5, Widget.Shape shape = Widget.Shape.Rectangle, Action < Widget> initMethod = null) + private Widget GetLimbEditWidget(string ID, Limb limb, int size = 5, WidgetShape shape = WidgetShape.Rectangle, Action < Widget> initMethod = null) { if (!limbEditWidgets.TryGetValue(ID, out Widget widget)) { @@ -5588,18 +5537,18 @@ namespace Barotrauma.CharacterEditor int selectedSize = (int)Math.Round(size * 1.5f); var w = new Widget(ID, size, shape) { - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10), - data = limb, - color = Color.Yellow, - secondaryColor = Color.Gray, - textColor = Color.Yellow + TooltipOffset = new Vector2(selectedSize / 2 + 5, -10), + Data = limb, + Color = Color.Yellow, + SecondaryColor = Color.Gray, + TextColor = Color.Yellow }; w.PreUpdate += dTime => w.Enabled = editLimbs && selectedLimbs.Contains(limb); w.PostUpdate += dTime => { - w.inputAreaMargin = w.IsControlled ? 1000 : 0; - w.size = w.IsSelected ? selectedSize : normalSize; - w.isFilled = w.IsControlled; + w.InputAreaMargin = w.IsControlled ? 1000 : 0; + w.Size = w.IsSelected ? selectedSize : normalSize; + w.IsFilled = w.IsControlled; }; w.MouseUp += () => RagdollParams.StoreSnapshot(); initMethod?.Invoke(w); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 64a6b65c9..5fb552402 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -13,7 +13,7 @@ namespace Barotrauma.CharacterEditor // Ragdoll data private Identifier name; private bool isHumanoid; - private bool canEnterSubmarine = true; + private CanEnterSubmarine canEnterSubmarine = CanEnterSubmarine.True; private bool canWalk; private string texturePath; private string xmlPath; @@ -153,6 +153,7 @@ namespace Barotrauma.CharacterEditor }; var topGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 1), frame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 2 }; var fields = new List(); + GUITextBox nameField = null; GUITextBox texturePathElement = null; GUITextBox xmlPathElement = null; GUIDropDown contentPackageDropDown = null; @@ -177,7 +178,7 @@ namespace Barotrauma.CharacterEditor { case 0: new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("Name")); - var nameField = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), Name.Value ?? GetCharacterEditorTranslation("DefaultName").Value) { CaretColor = Color.White }; + nameField = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), Name.Value ?? GetCharacterEditorTranslation("DefaultName").Value) { CaretColor = Color.White }; string ProcessText(string text) => text.RemoveWhitespace().CapitaliseFirstInvariant(); Name = ProcessText(nameField.Text).ToIdentifier(); nameField.OnTextChanged += (tb, text) => @@ -204,9 +205,14 @@ namespace Barotrauma.CharacterEditor var l = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), GetCharacterEditorTranslation("CanEnterSubmarines")); var t = new GUITickBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), string.Empty) { - Selected = CanEnterSubmarine, + //TODO: allow ternary selection (true, false, partial) + Selected = CanEnterSubmarine == CanEnterSubmarine.True, Enabled = !IsCopy, - OnSelected = (tB) => CanEnterSubmarine = tB.Selected + OnSelected = (tB) => + { + CanEnterSubmarine = tB.Selected ? CanEnterSubmarine.True : CanEnterSubmarine.False; + return true; + } }; if (!t.Enabled) { @@ -409,6 +415,12 @@ namespace Barotrauma.CharacterEditor return false; } + if (Name.Value.IsNullOrWhiteSpace()) + { + nameField?.Flash(useRectangleFlash: true); + return false; + } + string evaluatedTexturePath = ContentPath.FromRaw( contentPackageDropDown.SelectedData as ContentPackage, TexturePath).Value; @@ -905,7 +917,7 @@ namespace Barotrauma.CharacterEditor get => Instance.isHumanoid; set => Instance.isHumanoid = value; } - public bool CanEnterSubmarine + public CanEnterSubmarine CanEnterSubmarine { get => Instance.canEnterSubmarine; set => Instance.canEnterSubmarine = value; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs index 6e102be08..e0d94c1c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs @@ -440,13 +440,13 @@ namespace Barotrauma { widget.MouseDown += () => { - widget.color = GUIStyle.Green; + widget.Color = GUIStyle.Green; prevAngle = Rotation; disableMove = true; }; widget.Deselected += () => { - widget.color = Color.Yellow; + widget.Color = Color.Yellow; disableMove = false; }; widget.MouseHeld += (deltaTime) => @@ -484,7 +484,7 @@ namespace Barotrauma }; widget.PreDraw += (sprtBtch, deltaTime) => { - widget.tooltip = $"Scale: {Math.Round(Scale, 2)}\n" + + widget.Tooltip = $"Scale: {Math.Round(Scale, 2)}\n" + $"Rotation: {(int) MathHelper.ToDegrees(Rotation)}"; float rotation = Rotation - (float) Math.PI / 2f; widget.DrawPos = Position + new Vector2((float) Math.Cos(rotation), (float) Math.Sin(rotation)) * (Scale * widgetSize); @@ -519,17 +519,17 @@ namespace Barotrauma { if (!widgets.TryGetValue(id, out Widget? widget)) { - widget = new Widget(id, size, Widget.Shape.Rectangle) + widget = new Widget(id, size, WidgetShape.Rectangle) { - color = Color.Yellow, + Color = Color.Yellow, RequireMouseOn = false }; widgets.Add(id, widget); initMethod?.Invoke(widget); } - widget.size = size; - widget.thickness = thickness; + widget.Size = size; + widget.Thickness = thickness; return widget; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index e8806ca32..138112575 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -457,7 +457,14 @@ namespace Barotrauma } else { - connection.OverrideValue = ChangeType(attribute.Value, connection.ValueType); + try + { + connection.OverrideValue = ChangeType(attribute.Value, connection.ValueType); + } + catch + { + DebugConsole.ThrowError($"Failed to convert the value {attribute.Value} of the attribute {attribute.Name} to {connection.ValueType}."); + } } } } @@ -813,7 +820,6 @@ namespace Barotrauma Vector2 size = type == typeof(string) ? new Vector2(0.2f, 0.3f) : new Vector2(0.2f, 0.175f); var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.Edit"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") }, size, minSize: new Point(300, 175)); - Vector2 layoutSize = type == typeof(string) ? new Vector2(1f, 0.5f) : new Vector2(1f, 0.25f); var layout = new GUILayoutGroup(new RectTransform(layoutSize, msgBox.Content.RectTransform), isHorizontal: true); @@ -838,7 +844,7 @@ namespace Barotrauma valueInput.OnTextChanged += (component, o) => { Vector2 textSize = valueInput.Font.MeasureString(valueInput.WrappedText); - valueInput.RectTransform.NonScaledSize = new Point(valueInput.RectTransform.NonScaledSize.X, (int) textSize.Y + 10); + valueInput.RectTransform.NonScaledSize = new Point(valueInput.RectTransform.NonScaledSize.X, (int)textSize.Y + 10); listBox.UpdateScrollBarSize(); listBox.BarScroll = 1.0f; newValue = o; @@ -855,14 +861,31 @@ namespace Barotrauma return true; }; } - else if (type == typeof(float) || type == typeof(int)) + else if (type == typeof(float)) { - GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float) { FloatValue = (float) (newValue ?? 0.0f) }; + GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float); + if (newValue is float floatVal) + { + valueInput.FloatValue = floatVal; + } valueInput.OnValueChanged += component => { newValue = component.FloatValue; }; } + else if (type == typeof(int)) + { + GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Int); + if (newValue is int intVal) + { + valueInput.IntValue = intVal; + } + valueInput.OnValueChanged += component => { newValue = component.IntValue; }; + } else if (type == typeof(bool)) { - GUITickBox valueInput = new GUITickBox(new RectTransform(Vector2.One, layout.RectTransform), "Value") { Selected = (bool) (newValue ?? false) }; + GUITickBox valueInput = new GUITickBox(new RectTransform(Vector2.One, layout.RectTransform), "Value"); + if (newValue is bool val) + { + valueInput.Selected = val; + } valueInput.OnSelected += component => { newValue = component.Selected; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 7fc2923de..3f064e5dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -519,6 +519,8 @@ namespace Barotrauma } GraphicsQuad.Render(); + Character.DrawSpeechBubbles(spriteBatch, cam); + if (fadeToBlackState > 0.0f) { spriteBatch.Begin(SpriteSortMode.Deferred); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index e0a8da297..7606eefc9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -35,6 +35,8 @@ namespace Barotrauma private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; private readonly GUIDropDown selectedSubDropDown; + private readonly GUIDropDown selectedBeaconStationDropdown; + private readonly GUIDropDown selectedWreckDropdown; private Sprite editingSprite; @@ -195,6 +197,28 @@ namespace Barotrauma } subDropDownContainer.RectTransform.MinSize = new Point(0, selectedSubDropDown.RectTransform.MinSize.Y); + var beaconStationDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), beaconStationDropDownContainer.RectTransform), TextManager.Get("submarinetype.beaconstation")); + selectedBeaconStationDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), beaconStationDropDownContainer.RectTransform)); + selectedBeaconStationDropdown.AddItem(TextManager.Get("Any"), userData: null); + foreach (SubmarineInfo beaconStation in SubmarineInfo.SavedSubmarines) + { + if (beaconStation.Type != SubmarineType.BeaconStation) { continue; } + selectedBeaconStationDropdown.AddItem(beaconStation.DisplayName, userData: beaconStation); + } + beaconStationDropDownContainer.RectTransform.MinSize = new Point(0, selectedBeaconStationDropdown.RectTransform.MinSize.Y); + + var wreckDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), wreckDropDownContainer.RectTransform), TextManager.Get("submarinetype.wreck")); + selectedWreckDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), wreckDropDownContainer.RectTransform)); + selectedWreckDropdown.AddItem(TextManager.Get("Any"), userData: null); + foreach (SubmarineInfo wreck in SubmarineInfo.SavedSubmarines) + { + if (wreck.Type != SubmarineType.Wreck) { continue; } + selectedWreckDropdown.AddItem(wreck.DisplayName, userData: wreck); + } + wreckDropDownContainer.RectTransform.MinSize = new Point(0, selectedWreckDropdown.RectTransform.MinSize.Y); + mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedRightPanel.RectTransform), @@ -218,6 +242,8 @@ namespace Barotrauma GameMain.LightManager.ClearLights(); currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.ForceBeaconStation = selectedBeaconStationDropdown.SelectedData as SubmarineInfo; + currentLevelData.ForceWreck = selectedWreckDropdown.SelectedData as SubmarineInfo; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); @@ -269,7 +295,7 @@ namespace Barotrauma var nonPlayerFiles = ContentPackageManager.EnabledPackages.All.SelectMany(p => p .GetFiles() - .Where(f => !(f is SubmarineFile))).ToArray(); + .Where(f => f is not SubmarineFile)).ToArray(); SubmarineInfo subInfo = selectedSubDropDown.SelectedData as SubmarineInfo; subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && @@ -339,6 +365,9 @@ namespace Barotrauma currentLevelData = LevelData.CreateRandom(ToolBox.RandomSeed(10), generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; + currentLevelData.ForceBeaconStation = selectedBeaconStationDropdown.SelectedData as SubmarineInfo; + currentLevelData.ForceWreck = selectedWreckDropdown.SelectedData as SubmarineInfo; + currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); DebugConsole.NewMessage("*****************************************************************************"); @@ -967,7 +996,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item == null) { continue; } + if (item == null || item.HiddenInGame) { continue; } foreach (var light in item.GetComponents()) { light.Update((float)deltaTime, Cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs index f5e0e7ea5..3b667c6a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen/MainMenuScreen.cs @@ -152,7 +152,9 @@ namespace Barotrauma { SetMenuTabPositioning(); CreateHostServerFields(); + bool prevMenuOpen = GUI.SettingsMenuOpen; SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); + GUI.SettingsMenuOpen = prevMenuOpen; if (remoteContentDoc?.Root != null) { remoteContentContainer.ClearChildren(); @@ -1291,7 +1293,7 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); GameMain.GameSession = new GameSession(selectedSub, savePath, GameModePreset.SinglePlayerCampaign, settings, mapSeed); - GameMain.GameSession.CrewManager.CharacterInfos.Clear(); + GameMain.GameSession.CrewManager.ClearCharacterInfos(); foreach (var characterInfo in campaignSetupUI.CharacterMenus.Select(m => m.CharacterInfo)) { GameMain.GameSession.CrewManager.AddCharacterInfo(characterInfo); @@ -1390,7 +1392,7 @@ namespace Barotrauma float bannerAspectRatio = (float) playstyleBanner.Sprite.SourceRect.Width / playstyleBanner.Sprite.SourceRect.Height; playstyleBanner.RectTransform.NonScaledSize = new Point(playstyleBanner.Rect.Width, (int)(playstyleBanner.Rect.Width / bannerAspectRatio)); playstyleBanner.RectTransform.IsFixedSize = true; - new GUIFrame(new RectTransform(Vector2.One, playstyleBanner.RectTransform), "InnerGlow", color: Color.Black); + new GUIFrame(new RectTransform(playstyleBanner.Rect.Size + new Point(1), playstyleBanner.RectTransform, Anchor.Center), "InnerGlow", color: Color.Black); new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), playstyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.03f) }, "playstyle name goes here", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 207d259df..724a5836f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -53,7 +53,9 @@ namespace Barotrauma base.Select(); DeletePrevDownloads(); Reset(); - + + bool allowDownloads = GameMain.Client.ClientPeer is { AllowModDownloads: true }; + Frame.ClearChildren(); var mainVisibleFrame = new GUIFrame(new RectTransform((0.6f, 0.8f), Frame.RectTransform, Anchor.Center)); @@ -66,7 +68,7 @@ namespace Barotrauma "", font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft) { - TextGetter = () => GameMain.NetLobbyScreen.ServerName.Text + TextGetter = () => GameMain.Client.ServerName }; mainLayoutSpacing(); var downloadList = new GUIListBox(new RectTransform((1.0f, 0.76f), mainLayout.RectTransform)); @@ -164,7 +166,7 @@ namespace Barotrauma var msgBoxModList = new GUIListBox(new RectTransform(Vector2.One, innerLayout.RectTransform)); innerLayoutSpacing(0.05f); - var footer = textBlock(TextManager.Get("ModDownloadFooter"), GUIStyle.Font, Alignment.Center); + var footer = textBlock(TextManager.Get(allowDownloads ? "ModDownloadFooter" : "ModDownloadFooterFail"), GUIStyle.Font, Alignment.Center); innerLayoutSpacing(0.05f); GUILayoutGroup buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true); @@ -183,15 +185,28 @@ namespace Barotrauma } }; - buttonContainerSpacing(0.1f); - button(TextManager.Get("Yes"), () => confirmDownload = true); - buttonContainerSpacing(0.2f); - button(TextManager.Get("No"), () => + if (allowDownloads) { - GameMain.Client?.Quit(); - GameMain.MainMenuScreen.Select(); - }); - buttonContainerSpacing(0.1f); + buttonContainerSpacing(0.1f); + button(TextManager.Get("Yes"), () => confirmDownload = true); + buttonContainerSpacing(0.2f); + button(TextManager.Get("No"), () => + { + GameMain.Client?.Quit(); + GameMain.MainMenuScreen.Select(); + }); + buttonContainerSpacing(0.1f); + } + else + { + buttonContainerSpacing(0.15f); + button(TextManager.Get("Cancel"), () => + { + GameMain.Client?.Quit(); + GameMain.MainMenuScreen.Select(); + }, width: 0.7f); + buttonContainerSpacing(0.15f); + } var missingIds = missingPackages .Where(p => p.IsMandatory) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index af8541d80..14bd2d8a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; @@ -12,18 +13,16 @@ namespace Barotrauma { partial class NetLobbyScreen : Screen { - private readonly GUIFrame infoFrame, modeFrame; - private readonly GUILayoutGroup infoFrameContent; - private readonly GUIFrame myCharacterFrame; - - private readonly GUIListBox chatBox; - private readonly GUIButton serverLogReverseButton; - private readonly GUIListBox serverLogBox, serverLogFilterTicks; + private GUIListBox chatBox; + private GUIButton serverLogReverseButton; + private GUIListBox serverLogBox, serverLogFilterTicks; private GUIComponent jobVariantTooltip; - private readonly GUITextBox chatInput; - private readonly GUITextBox serverLogFilter; + private GUIComponent playStyleIconContainer; + + private GUITextBox chatInput; + private GUITextBox serverLogFilter; public GUITextBox ChatInput { get @@ -32,63 +31,52 @@ namespace Barotrauma } } - private readonly GUIImage micIcon; + private GUIImage micIcon; - private readonly GUIScrollBar levelDifficultyScrollBar; + private GUIScrollBar levelDifficultySlider; private readonly List traitorElements = new List(); - private readonly GUIScrollBar traitorProbabilitySlider; - private readonly GUITextBlock traitorProbabilityText; - private readonly GUILayoutGroup traitorDangerGroup; + private GUIScrollBar traitorProbabilitySlider; + private GUILayoutGroup traitorDangerGroup; - private readonly GUIButton[] botCountButtons; - private readonly GUITextBlock botCountText; + public GUIFrame MissionTypeFrame { get; private set; } + public GUIFrame CampaignSetupFrame { get; private set; } + public GUIFrame CampaignFrame { get; private set; } - private readonly GUIButton[] botSpawnModeButtons; - private readonly GUITextBlock botSpawnModeText; + public GUIButton QuitCampaignButton { get; private set; } - public readonly GUIFrame MissionTypeFrame; - public readonly GUIFrame CampaignSetupFrame; - public readonly GUIFrame CampaignFrame; - public readonly GUIButton ContinueCampaignButton, QuitCampaignButton; + private GUITickBox[] missionTypeTickBoxes; + private GUIListBox missionTypeList; - private readonly GUITickBox[] missionTypeTickBoxes; - private readonly GUIListBox missionTypeList; + public GUITextBox LevelSeedBox { get; private set; } - public GUITextBox SeedBox - { - get; private set; - } + private GUIButton joinOnGoingRoundButton; + /// + /// Contains the elements that control starting the round (start button, spectate button, "ready to start" tickbox) + /// + private GUILayoutGroup roundControlsHolder; - private readonly GUIComponent gameModeContainer; - private readonly GUIButton spectateButton; - private readonly GUILayoutGroup roundControlsHolder; + public GUIButton SettingsButton { get; private set; } + public static GUIButton JobInfoFrame { get; set; } - public readonly GUIButton SettingsButton; - public static GUIButton JobInfoFrame; - - private readonly GUITickBox spectateBox; + private GUITickBox spectateBox; public bool Spectating => spectateBox is { Selected: true, Visible: true }; - private readonly GUIFrame playerInfoContainer; - - private GUILayoutGroup infoContainer; + private GUILayoutGroup playerInfoContent; private GUIComponent changesPendingText; private bool createPendingChangesText = true; - public GUIButton PlayerFrame; + public GUIButton PlayerFrame { get; private set; } - public readonly GUIButton SubVisibilityButton; + public GUIButton SubVisibilityButton { get; private set; } - private readonly GUITextBox subSearchBox; + private GUITextBox subSearchBox; - private readonly GUIComponent subPreviewContainer; + private GUIComponent subPreviewContainer; - private readonly GUITickBox autoRestartBox; - private readonly GUITextBlock autoRestartText; + private GUITickBox autoRestartBox; + private GUITextBlock autoRestartText; - private readonly GUITickBox shuttleTickBox; - - private readonly GUIComponent settingsBlocker; + private GUITickBox shuttleTickBox; private Sprite backgroundSprite; @@ -98,11 +86,14 @@ namespace Barotrauma private GUIFrame characterInfoFrame; private GUIFrame appearanceFrame; - public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu; - public GUIFrame JobSelectionFrame; + private readonly List respawnSettingsElements = new List(); + private readonly List campaignDisabledElements = new List(); - public GUIFrame JobPreferenceContainer; - public GUIListBox JobList; + public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; } + public GUIFrame JobSelectionFrame { get; private set; } + + public GUIFrame JobPreferenceContainer { get; private set; } + public GUIListBox JobList { get; private set; } private Identifier micIconStyle; private float micCheckTimer; @@ -119,52 +110,39 @@ namespace Barotrauma set; } - //elements that can only be used by the host + /// + /// Elements that can only be used by the host or people with server settings management permissions (but are visible to everyone) + /// private readonly List clientDisabledElements = new List(); - //elements that can't be interacted with but don't look disabled - private readonly List clientReadonlyElements = new List(); - //elements that aren't shown client-side + + /// + /// Elements that are only visible to the host or people with server settings management permissions + /// private readonly List clientHiddenElements = new List(); + private readonly List botSettingsElements = new List(); + + private readonly Dictionary settingAssignedComponents = new Dictionary(); + public GUIComponent FileTransferFrame { get; private set; } public GUITextBlock FileTransferTitle { get; private set; } public GUIProgressBar FileTransferProgressBar { get; private set; } public GUITextBlock FileTransferProgressText { get; private set; } - public GUITextBox ServerName - { - get; - private set; - } + public GUITickBox Favorite { get; private set; } - public GUITickBox Favorite - { - get; - private set; - } + public GUILayoutGroup LogButtons { get; private set; } - public GUITextBox ServerMessage - { - get; - private set; - } + /// + /// Tab buttons above the chat panel (chat and server log tabs) + /// + private readonly List chatPanelTabButtons = new List(); - public GUILayoutGroup LogButtons - { - get; - private set; - } + private GUITextBlock publicOrPrivateText, playstyleText; - private readonly GUIButton showChatButton; - private readonly GUIButton showLogButton; - - private readonly GUITextBlock publicOrPrivate; - - public readonly GUIListBox SubList; - - public readonly GUIDropDown ShuttleList; - - public readonly GUIListBox ModeList; + public GUIListBox SubList { get; private set; } + public GUIDropDown ShuttleList { get; private set; } + public GUIListBox ModeList { get; private set; } private int selectedModeIndex; public int SelectedModeIndex @@ -189,35 +167,20 @@ namespace Barotrauma } } + //No, this should not be static even though your IDE might say so! There's a server-side version of this which needs to be an instance method. public IReadOnlyList GetSubList() => (IReadOnlyList)GameMain.Client?.ServerSubmarines ?? Array.Empty(); - public readonly GUIListBox PlayerList; + public GUIListBox PlayerList; - public GUITextBox CharacterNameBox - { - get; - private set; - } + public GUITextBox CharacterNameBox { get; private set; } - public GUIListBox TeamPreferenceListBox - { - get; - private set; - } + public GUIListBox TeamPreferenceListBox { get; private set; } - public GUIButton StartButton - { - get; - private set; - } + public GUIButton StartButton { get; private set; } - public GUITickBox ReadyToStartBox - { - get; - private set; - } + public GUITickBox ReadyToStartBox { get; private set; } public SubmarineInfo SelectedSub => SubList.SelectedData as SubmarineInfo; @@ -282,7 +245,7 @@ namespace Barotrauma List jobPreferences = new List(); foreach (GUIComponent child in JobList.Content.Children) { - if (!(child.UserData is JobVariant jobPrefab)) { continue; } + if (child.UserData is not JobVariant jobPrefab) { continue; } jobPreferences.Add(jobPrefab); } return jobPreferences; @@ -297,130 +260,962 @@ namespace Barotrauma } set { - if (levelSeed == value) return; + if (levelSeed == value) { return; } levelSeed = value; int intSeed = ToolBox.StringToInt(levelSeed); backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed); - SeedBox.Text = levelSeed; + LevelSeedBox.Text = levelSeed; } } + private const float MainPanelWidth = 0.7f; + private const float SidePanelWidth = 0.3f; + /// + /// Spacing between different elements in the panels + /// + private const float PanelSpacing = 0.005f; + + /// + /// Size of the outer border of the panels (= empty area round the contents of the panel) + /// + private static int PanelBorderSize => GUI.IntScale(20); + + private static Point GetSizeWithoutBorder(GUIComponent parent) => new Point(parent.Rect.Width - PanelBorderSize * 2, parent.Rect.Height - PanelBorderSize * 2); + public NetLobbyScreen() { - float panelSpacing = 0.005f; - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center) { MaxSize = new Point(int.MaxValue, GameMain.GraphicsHeight - 50) }, isHorizontal: false) + var contentArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center), isHorizontal: false) { Stretch = true, - RelativeSpacing = panelSpacing + RelativeSpacing = PanelSpacing }; - var panelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), innerFrame.RectTransform, Anchor.Center), isHorizontal: true) + var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, contentArea.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, - RelativeSpacing = panelSpacing + RelativeSpacing = PanelSpacing }; - GUILayoutGroup panelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), panelContainer.RectTransform)) + var mainPanel = new GUIFrame(new RectTransform(new Vector2(MainPanelWidth, 1.0f), horizontalLayout.RectTransform)); + + var mainPanelLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width, mainPanel.Rect.Height - PanelBorderSize), mainPanel.RectTransform, Anchor.TopCenter), childAnchor: Anchor.TopCenter) { Stretch = true, - RelativeSpacing = panelSpacing + //more spacing to more clearly separate the top and bottom + RelativeSpacing = PanelSpacing * 4 }; - GUILayoutGroup bottomBar = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerFrame.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - GUILayoutGroup bottomBarLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - GUILayoutGroup bottomBarMid = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - GUILayoutGroup bottomBarRight = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) - { - Stretch = true, - IsHorizontal = true, - RelativeSpacing = panelSpacing - }; - - //server info panel ------------------------------------------------------------ - - infoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), panelHolder.RectTransform)); - infoFrameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoFrame.RectTransform, Anchor.Center)) + GUILayoutGroup serverInfoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), mainPanelLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.025f }; + CreateServerInfoContents(serverInfoHolder); - //server game panel ------------------------------------------------------------ - - modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), panelHolder.RectTransform)) - { - CanBeFocused = false - }; - - gameModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), modeFrame.RectTransform, Anchor.Center)) - { - RelativeSpacing = panelSpacing * 4.0f, - Stretch = true - }; - - var disconnectButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect")) - { - OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; } - }; - disconnectButton.TextBlock.AutoScaleHorizontal = true; - - // file transfers ------------------------------------------------------------ - FileTransferFrame = new GUIFrame(new RectTransform(Vector2.One, bottomBarLeft.RectTransform), style: "TextFrame"); - var fileTransferContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), FileTransferFrame.RectTransform, Anchor.Center)) + var mainPanelTopLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = PanelSpacing }; - FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont); - var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + + var mainPanelBottomLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true) { + Stretch = true, + RelativeSpacing = PanelSpacing + }; + + //-------------------------------------------------------------------------------------------------------------------------------- + //top panel (game mode, submarine) + //-------------------------------------------------------------------------------------------------------------------------------- + + CreateGameModeDropdown(mainPanelTopLayout); + CreateSubmarineListPanel(mainPanelTopLayout); + CreateSubmarineInfoPanel(mainPanelTopLayout); + + //-------------------------------------------------------------------------------------------------------------------------------- + //bottom panel (settings) + //-------------------------------------------------------------------------------------------------------------------------------- + + CreateGameModePanel(mainPanelBottomLayout); + CreateGameModeSettingsPanel(mainPanelBottomLayout); + CreateGeneralSettingsPanel(mainPanelBottomLayout); + mainPanelBottomLayout.Recalculate(); + + foreach (var child in mainPanelBottomLayout.GetAllChildren()) + { + if (traitorDangerGroup.Children.Contains(child)) + { + //don't touch the colors of the traitor danger indicators, they're intentionally very dim when disabled + continue; + } + //make the disabled colors slightly less dim (these should be readable, despite being non-interactable) + child.DisabledColor = new Color(child.Color, child.Color.A / 255.0f * 0.8f); + if (child is GUITextBlock textBlock) + { + textBlock.DisabledTextColor = new Color(textBlock.TextColor, textBlock.TextColor.A / 255.0f * 0.8f); + } + } + + //-------------------------------------------------------------------------------------------------------------------------------- + //right panel (Character customization/Chat) + //-------------------------------------------------------------------------------------------------------------------------------- + + var sidePanel = new GUIFrame(new RectTransform(new Vector2(SidePanelWidth, 1.0f), horizontalLayout.RectTransform)); + GUILayoutGroup sidePanelLayout = new GUILayoutGroup(new RectTransform(GetSizeWithoutBorder(sidePanel), + sidePanel.RectTransform, Anchor.Center)) + { + RelativeSpacing = PanelSpacing * 4, Stretch = true }; - FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); - FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", - font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); - new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") + + CreateSidePanelContents(sidePanelLayout); + + //-------------------------------------------------------------------------------------------------------------------------------- + // bottom panel (start round, quit, transfers, ready to start...) ------------------------------------------------------------ + //-------------------------------------------------------------------------------------------------------------------------------- + GUILayoutGroup bottomBar = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), contentArea.RectTransform), childAnchor: Anchor.CenterLeft) { - OnClicked = (btn, userdata) => + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + CreateBottomPanelContents(bottomBar); + } + + private void AssignComponentToServerSetting(GUIComponent component, string settingName) + { + settingAssignedComponents[component] = settingName; + } + + public void AssignComponentsToServerSettings() + { + settingAssignedComponents.ForEach(kvp => GameMain.Client.ServerSettings.AssignGUIComponent(kvp.Value, kvp.Key)); + } + + private void CreateServerInfoContents(GUIComponent parent) + { + GUIFrame serverInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); + var serverBanner = new GUICustomComponent(new RectTransform(Vector2.One, serverInfoFrame.RectTransform), DrawServerBanner) + { + HideElementsOutsideFrame = true, + IgnoreLayoutGroups = true + }; + + GUIFrame serverInfoContent = new GUIFrame(new RectTransform(new Vector2(0.98f, 0.9f), serverInfoFrame.RectTransform, Anchor.Center), style: null); + + var serverLabelContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.05f), serverInfoContent.RectTransform), isHorizontal: true) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + playstyleText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform), + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); + publicOrPrivateText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform), + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); + + var serverNameShadow = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, + string.Empty, font: GUIStyle.LargeFont, textColor: Color.Black) + { + IgnoreLayoutGroups = true + }; + var serverName = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft), + string.Empty, font: GUIStyle.LargeFont, textColor: GUIStyle.TextColorBright) + { + IgnoreLayoutGroups = true, + TextGetter = serverNameShadow.TextGetter = () => GameMain.Client?.ServerName + }; + + playStyleIconContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.4f), serverInfoContent.RectTransform, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.BottomRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + Favorite = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.5f), serverInfoContent.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight), + "", null, "GUIServerListFavoriteTickBox") + { + IgnoreLayoutGroups = true, + Selected = false, + ToolTip = TextManager.Get("addtofavorites"), + OnSelected = (tickbox) => { - if (!(FileTransferFrame.UserData is FileReceiver.FileTransferIn transfer)) { return false; } - GameMain.Client?.CancelFileTransfer(transfer); - GameMain.Client?.FileReceiver.StopTransfer(transfer); + if (GameMain.Client == null) { return true; } + ServerInfo info = GameMain.Client.CreateServerInfoFromSettings(); + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(info); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(info); + } + tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); return true; } }; - // Sidebar area (Character customization/Chat) + SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), serverInfoContent.RectTransform, Anchor.TopRight), + TextManager.Get("ServerSettingsButton"), style: "GUIButtonSmall"); + } - GUILayoutGroup sideBar = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), panelContainer.RectTransform, maxSize: new Point(650, panelContainer.RectTransform.Rect.Height))) + public void RefreshPlaystyleIcons() + { + playStyleIconContainer?.ClearChildren(); + if (GameMain.Client?.ClientPeer?.ServerConnection is not { } serverConnection || serverConnection.Endpoint == null) { return; } + var serverInfo = ServerInfo.FromServerEndpoints(serverConnection.Endpoint.ToEnumerable().ToImmutableArray(), GameMain.Client.ServerSettings); + + var playStyleTags = serverInfo.GetPlayStyleTags(); + foreach (var tag in playStyleTags) + { + var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}") + ?.GetSprite(GUIComponent.ComponentState.None); + if (playStyleIcon is null) { continue; } + + new GUIImage(new RectTransform(Vector2.One, playStyleIconContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + playStyleIcon, scaleToFit: true) + { + ToolTip = TextManager.Get($"servertagdescription.{tag}"), + Color = Color.White + }; + } + } + + private void CreateGameModeDropdown(GUIComponent parent) + { + //------------------------------------------------------------------------------------------------------------------ + // Gamemode panel + //------------------------------------------------------------------------------------------------------------------ + + GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.005f + }; + + var modeLabel = CreateSubHeader("GameMode", gameModeHolder); + var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), + TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) + { + UserData = "modevotes", + Visible = false + }; + ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) + { + PlaySoundOnSelect = true, + OnSelected = VotableClicked + }; + + foreach (GameModePreset mode in GameModePreset.List) + { + if (mode.IsSinglePlayer) { continue; } + + var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), ModeList.Content.RectTransform), style: null) + { + UserData = mode + }; + + var modeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.76f, 0.9f), modeFrame.RectTransform, Anchor.CenterRight)) + { + AbsoluteSpacing = GUI.IntScale(5), + Stretch = true + }; + + var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); + modeTitle.RectTransform.NonScaledSize = new Point(int.MaxValue, (int)modeTitle.TextSize.Y); + modeTitle.RectTransform.IsFixedSize = true; + var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); + //leave some padding for the vote count text + modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); + modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; + modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; + modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; + modeFrame.OnAddedToGUIUpdateList = (c) => + { + modeTitle.State = modeDescription.State = c.State; + }; + modeDescription.RectTransform.SizeChanged += () => + { + modeDescription.RectTransform.NonScaledSize = new Point(modeDescription.Rect.Width, (int)modeDescription.TextSize.Y); + modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); + }; + + new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, + style: "GameModeIcon." + mode.Identifier, scaleToFit: true); + } + } + + private void CreateSubmarineListPanel(GUIComponent parent) + { + var submarineListHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.005f + }; + + var subLabel = CreateSubHeader("Submarine", submarineListHolder); + SubVisibilityButton + = new GUIButton( + new RectTransform(Vector2.One * 1.2f, subLabel.RectTransform, anchor: Anchor.CenterRight, + scaleBasis: ScaleBasis.BothHeight) + { AbsoluteOffset = new Point(0, GUI.IntScale(5)) }, + style: "EyeButton") + { + OnClicked = (button, o) => + { + CreateSubmarineVisibilityMenu(); + return false; + } + }; + clientHiddenElements.Add(SubVisibilityButton); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), submarineListHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); + subSearchBox = new GUITextBox(new RectTransform(Vector2.One, filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; + subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + subSearchBox.OnTextChanged += (textBox, text) => + { + UpdateSubVisibility(); + return true; + }; + + SubList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.93f), submarineListHolder.RectTransform)) + { + PlaySoundOnSelect = true, + OnSelected = VotableClicked + }; + + var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subLabel.RectTransform, Anchor.TopRight), + TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) + { + UserData = "subvotes", + Visible = false, + CanBeFocused = false + }; + } + + private void CreateSubmarineInfoPanel(GUIComponent parent) + { + var submarineInfoHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.005f + }; + //submarine preview ------------------------------------------------------------------ + + subPreviewContainer = new GUIFrame(new RectTransform(Vector2.One, submarineInfoHolder.RectTransform), style: null); + subPreviewContainer.RectTransform.SizeChanged += () => + { + if (SelectedSub != null) { CreateSubPreview(SelectedSub); } + }; + } + + private GUIComponent CreateGameModePanel(GUIComponent parent) + { + var gameModeSpecificFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); + CampaignSetupFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) + { + Visible = false + }; + CampaignFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) + { + Visible = false + }; + GUILayoutGroup campaignContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.5f), CampaignFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), + TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + + QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), + TextManager.Get("quitbutton"), textAlignment: Alignment.Center) + { + OnClicked = (_, __) => + { + if (GameMain.Client == null) { return false; } + if (GameMain.Client.GameStarted) + { + GameMain.Client.RequestRoundEnd(save: false); + } + else + { + GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true); + } + return true; + } + }; + + //mission type ------------------------------------------------------------------ + MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null); + + GUILayoutGroup missionHolder = new GUILayoutGroup(new RectTransform(Vector2.One, MissionTypeFrame.RectTransform)) { - RelativeSpacing = panelSpacing, Stretch = true }; + CreateSubHeader("MissionType", missionHolder); + missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) + { + OnSelected = (component, obj) => + { + return false; + } + }; + clientDisabledElements.Add(missionTypeList); + + var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); + missionTypeTickBoxes = new GUITickBox[missionTypes.Length - 2]; + int index = 0; + for (int i = 0; i < missionTypes.Length; i++) + { + var missionType = missionTypes[i]; + if (missionType == MissionType.None || missionType == MissionType.All) { continue; } + + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, GUI.IntScale(30)) }, style: null) + { + UserData = missionType, + }; + + if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) + { + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), string.Empty) + { + UserData = (int)missionType, + Visible = false, + CanBeFocused = false + }; + } + else + { + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), + TextManager.Get("MissionType." + missionType.ToString())) + { + UserData = (int)missionType, + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), + OnSelected = (tickbox) => + { + int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; + int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); + return true; + } + }; + frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; + } + index++; + } + clientDisabledElements.AddRange(missionTypeTickBoxes); + + return gameModeSpecificFrame; + } + + private GUIComponent CreateGameModeSettingsPanel(GUIComponent parent) + { + //------------------------------------------------------------------ + // settings panel + //------------------------------------------------------------------ + + GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true + }; + CreateSubHeader("GameModeSettings", settingsLayout); + + var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content; + + //seed ------------------------------------------------------------------ + + var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("LevelSeed")) + { + CanBeFocused = false + }; + LevelSeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); + LevelSeedBox.OnDeselected += (textBox, key) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); + }; + campaignDisabledElements.Add(LevelSeedBox); + campaignDisabledElements.Add(seedLabel); + clientDisabledElements.Add(LevelSeedBox); + clientDisabledElements.Add(seedLabel); + LevelSeed = ToolBox.RandomSeed(8); + + //level difficulty ------------------------------------------------------------------ + + var levelDifficultyHolder = CreateLabeledSlider(settingsContent, "LevelDifficulty", "", "LevelDifficultyExplanation", out levelDifficultySlider, out var difficultySliderLabel, + step: 0.01f, range: new Vector2(0.0f, 100.0f)); + levelDifficultySlider.OnReleased = (scrollbar, value) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + levelDifficultySlider.OnMoved = (scrollbar, value) => + { + if (!EventManagerSettings.Prefabs.Any()) { return true; } + difficultySliderLabel.Text = + EventManagerSettings.GetByDifficultyPercentile(value).Name + + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})"; + difficultySliderLabel.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + return true; + }; + AssignComponentToServerSetting(levelDifficultySlider, nameof(ServerSettings.SelectedLevelDifficulty)); + campaignDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren()); + clientDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren()); + + //bot count ------------------------------------------------------------------ + CreateSubHeader("BotSettings", settingsContent); + + var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); + var botCountSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform)); + for (int i = 0; i <= NetConfig.MaxPlayers; i++) + { + botCountSelection.AddElement(i, i.ToString()); + } + AssignComponentToServerSetting(botCountSelection, nameof(ServerSettings.BotCount)); + clientDisabledElements.AddRange(botCountSettingHolder.GetAllChildren()); + botSettingsElements.Add(botCountSelection); + + var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); + var botSpawnModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform)); + foreach (var botSpawnMode in Enum.GetValues(typeof(BotSpawnMode)).Cast()) + { + botSpawnModeSelection.AddElement(botSpawnMode, botSpawnMode.ToString(), TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip")); + } + botSpawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + AssignComponentToServerSetting(botSpawnModeSelection, nameof(ServerSettings.BotSpawnMode)); + clientDisabledElements.AddRange(botSpawnModeSettingHolder.GetAllChildren()); + botSettingsElements.Add(botSpawnModeSelection); + + botCountSelection.OnValueChanged += (_) => + { + botSpawnModeSelection.Enabled = GameMain.Client.ServerSettings.BotCount > 0; + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + }; + + //traitor probability ------------------------------------------------------------------ + + CreateSubHeader("TraitorSettings", settingsContent); + + //spacing + new GUIFrame(new RectTransform(new Point(1, GUI.IntScale(5)), settingsContent.RectTransform), style: null); + + //the probability slider is a traitor element, but we don't add it to traitorElements + //because we don't want to disable it when sliding it to 0 (need to be able to slide it back!) + var traitorProbabilityHolder = CreateLabeledSlider(settingsContent, "traitor.probability", "", "traitor.probability.tooltip", + out traitorProbabilitySlider, out var traitorProbabilityText, + step: 0.01f, range: new Vector2(0.0f, 1.0f)); + traitorProbabilitySlider.OnMoved = (scrollbar, value) => + { + traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString()); + traitorProbabilityText.TextColor = + value <= 0.0f ? + GUIStyle.Green : + ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red); + RefreshEnabledElements(); + return true; + }; + traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll); + traitorProbabilitySlider.OnReleased += (scrollbar, value) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; + AssignComponentToServerSetting(traitorProbabilitySlider, nameof(ServerSettings.TraitorProbability)); + traitorElements.Clear(); + clientDisabledElements.AddRange(traitorProbabilityHolder.GetAllChildren()); + + var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + var dangerLevelLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true) + { + ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip") + }; + + var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + var traitorDangerButtons = new GUIButton[2]; + traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1); + return true; + } + }; + + traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + AbsoluteSpacing = 1 + }; + for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++) + { + var difficultyColor = Mission.GetDifficultyColor(i); + new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true) + { + ToolTip = + RichString.Rich( + $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' + + TextManager.Get($"traitor.dangerlevel.{i}.description")), + Color = difficultyColor, + DisabledColor = Color.Gray * 0.5f, + }; + } + + traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight") + { + OnClicked = (button, obj) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1); + return true; + } + }; + + traitorDangerContainer.InheritTotalChildrenMinHeight(); + SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel); + traitorElements.Add(dangerLevelLabel); + traitorElements.AddRange(traitorDangerGroup.Children); + traitorElements.AddRange(traitorDangerButtons); + + var traitorsMinPlayerCountHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), traitorsMinPlayerCountHolder.RectTransform), TextManager.Get("ServerSettingsTraitorsMinPlayerCount"), wrap: true) + { + ToolTip = TextManager.Get("ServerSettingsTraitorsMinPlayerCountToolTip") + }; + var traitorsMinPlayerCount = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), traitorsMinPlayerCountHolder.RectTransform)); + for (int i = 1; i <= NetConfig.MaxPlayers; i++) + { + traitorsMinPlayerCount.AddElement(i, i.ToString()); + } + traitorsMinPlayerCount.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + AssignComponentToServerSetting(traitorsMinPlayerCount, nameof(ServerSettings.TraitorsMinPlayerCount)); + traitorElements.AddRange(traitorsMinPlayerCountHolder.Children); + + foreach (var traitorElement in traitorElements) + { + if (!clientDisabledElements.Contains(traitorElement)) + { + clientDisabledElements.Add(traitorElement); + } + } + + return settingsContent; + } + + private GUIComponent CreateGeneralSettingsPanel(GUIComponent parent) + { + //------------------------------------------------------------------ + // settings panel + //------------------------------------------------------------------ + + GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) + { + Stretch = true + }; + var respawnSettingsHeader = CreateSubHeader("RespawnSettings", settingsLayout); + + var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content; + + // ------------------------------------------------------------------ + + var respawnBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform) { AbsoluteOffset = new Point((int)respawnSettingsHeader.Padding.X, 0) }, + TextManager.Get("ServerSettingsAllowRespawning")) + { + ToolTip = TextManager.Get("RespawnExplanation"), + OnSelected = (tickbox) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + RefreshEnabledElements(); + return true; + } + }; + AssignComponentToServerSetting(respawnBox, nameof(ServerSettings.AllowRespawn)); + clientDisabledElements.Add(respawnBox); + + GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform), isHorizontal: true) + { + Stretch = true + }; + + shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) + { + ToolTip = TextManager.Get("RespawnShuttleExplanation"), + Selected = true, + OnSelected = (GUITickBox box) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + } + }; + AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle)); + respawnSettingsElements.Add(shuttleTickBox); + + shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => + { + shuttleTickBox.TextBlock.AutoScaleHorizontal = true; + shuttleTickBox.TextBlock.TextScale = 1.0f; + if (shuttleTickBox.TextBlock.TextScale < 0.75f) + { + shuttleTickBox.TextBlock.Wrap = true; + shuttleTickBox.TextBlock.AutoScaleHorizontal = true; + shuttleTickBox.TextBlock.TextScale = 1.0f; + } + }; + ShuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) + { + OnSelected = (component, obj) => + { + GameMain.Client?.RequestSelectSub(obj as SubmarineInfo, isShuttle: true); + return true; + } + }; + ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); + shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); + respawnSettingsElements.Add(ShuttleList); + + var respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel, + range: new Vector2(10.0f, 600.0f)); + LocalizedString intervalLabel = respawnIntervalSliderLabel.Text; + respawnIntervalSlider.StepValue = 10.0f; + respawnIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = intervalLabel + " " + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + respawnIntervalSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll); + respawnSettingsElements.AddRange(respawnIntervalElement.GetAllChildren()); + AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval)); + + var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel, + step: 0.1f, range: new Vector2(0.0f, 1.0f)); + minRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = ToolBox.GetFormattedPercentage(scrollBar.BarScrollValue); + return true; + }; + minRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll); + respawnSettingsElements.AddRange(minRespawnElement.GetAllChildren()); + AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio)); + + var respawnDurationElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnDuration", "", "ServerSettingsRespawnDurationTooltip", out var respawnDurationSlider, out var respawnDurationSliderLabel, + step: 0.1f, range: new Vector2(60.0f, 660.0f)); + respawnDurationSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = scrollBar.BarScrollValue <= 0 ? TextManager.Get("Unlimited") : ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); + return true; + }; + respawnDurationSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnDurationSlider.ScrollToValue = (GUIScrollBar scrollBar, float barScroll) => + { + return barScroll >= 1.0f ? 0.0f : barScroll * (scrollBar.Range.Y - scrollBar.Range.X) + scrollBar.Range.X; + }; + respawnDurationSlider.ValueToScroll = (GUIScrollBar scrollBar, float value) => + { + return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X); + }; + respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll); + respawnSettingsElements.AddRange(respawnDurationElement.GetAllChildren()); + AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime)); + + var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip", + out var skillLossSlider, out var skillLossSliderLabel, range: new Vector2(0, 100)); + skillLossSlider.StepValue = 1; + skillLossSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString()); + return true; + }; + skillLossSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnSettingsElements.AddRange(skillLossElement.GetAllChildren()); + AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath)); + skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); + + var skillLossImmediateRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnImmediateRespawn", "", "ServerSettingsSkillLossPercentageOnImmediateRespawnToolTip", + out var skillLossImmediateRespawnSlider, out var skillLossImmediateRespawnSliderLabel, range: new Vector2(0, 100)); + skillLossImmediateRespawnSlider.StepValue = 1; + skillLossImmediateRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => + { + GUITextBlock text = scrollBar.UserData as GUITextBlock; + text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString()); + return true; + }; + skillLossImmediateRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => + { + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); + return true; + }; + respawnSettingsElements.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); + AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn)); + skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll); + + foreach (var respawnElement in respawnSettingsElements) + { + if (!clientDisabledElements.Contains(respawnElement)) + { + clientDisabledElements.Add(respawnElement); + } + } + + return settingsContent; + } + + public static GUITextBlock CreateSubHeader(string textTag, GUIComponent parent, string toolTipTag = null) + { + var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), parent.RectTransform) { MinSize = new Point(0, GUI.IntScale(28)) }, + TextManager.Get(textTag), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft, textColor: GUIStyle.TextColorBright) + { + CanBeFocused = false + }; + if (!toolTipTag.IsNullOrEmpty()) + { + header.ToolTip = TextManager.Get(toolTipTag); + } + return header; + } + + public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, out GUIScrollBar slider, out GUITextBlock label, float? step = null, Vector2? range = null) + { + GUILayoutGroup verticalLayout = null; + if (!headerTag.IsNullOrEmpty()) + { + verticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), isHorizontal: false) + { + Stretch = true + }; + var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), verticalLayout.RectTransform), + TextManager.Get(headerTag), textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + header.RectTransform.MinSize = new Point(0, (int)header.TextSize.Y); + } + + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, headerTag == null ? 0.0f : 0.5f), (verticalLayout ?? parent).RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + //spacing + new GUIFrame(new RectTransform(new Point(GUI.IntScale(5), 0), container.RectTransform), style: null); + + slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider"); + if (step.HasValue) { slider.Step = step.Value; } + if (range.HasValue) { slider.Range = range.Value; } + + container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y); + container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y); + verticalLayout?.InheritTotalChildrenMinHeight(); + + label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform, Anchor.CenterRight), + string.IsNullOrEmpty(valueLabelTag) ? "" : TextManager.Get(valueLabelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) + { + CanBeFocused = false + }; + + //slider has a reference to the label to change the text when it's used + slider.UserData = label; + + slider.ToolTip = label.ToolTip = TextManager.Get(tooltipTag); + return verticalLayout ?? container; + } + + public static GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag = null, GUIFont font = null) + { + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f, + ToolTip = TextManager.Get(labelTag) + }; + + var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), + TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: font) + { + AutoScaleHorizontal = true + }; + if (!string.IsNullOrEmpty(toolTipTag)) + { + label.ToolTip = TextManager.Get(toolTipTag); + } + var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) + { + MinValueInt = min, + MaxValueInt = max + }; + + container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); + container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); + + return input; + } + + public static GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag = null) + { + var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f, + ToolTip = TextManager.Get(labelTag) + }; + + var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), + TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft) + { + AutoScaleHorizontal = true + }; + if (!string.IsNullOrEmpty(toolTipTag)) + { + label.ToolTip = TextManager.Get(toolTipTag); + } + var input = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), elementCount: numElements); + + container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); + container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); + + return input; + } + + private void CreateSidePanelContents(GUIComponent rightPanel) + { //player info panel ------------------------------------------------------------ - myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); - playerInfoContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), myCharacterFrame.RectTransform, Anchor.Center), style: null); + var myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), rightPanel.RectTransform), style: null); + var myCharacterContent = new GUILayoutGroup(new RectTransform(new Vector2(1), myCharacterFrame.RectTransform, Anchor.Center)) + { + Stretch = true + }; - spectateBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.06f), myCharacterFrame.RectTransform) { RelativeOffset = new Vector2(0.05f, 0.05f) }, + spectateBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.06f), myCharacterContent.RectTransform), TextManager.Get("spectatebutton")) { Selected = false, @@ -428,17 +1223,22 @@ namespace Barotrauma UserData = "spectate" }; + playerInfoContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), myCharacterContent.RectTransform)) + { + Stretch = true + }; + // Social area - GUIFrame logBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); - GUILayoutGroup logHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), logBackground.RectTransform, Anchor.Center)) + GUIFrame logFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightPanel.RectTransform), style: null); + GUILayoutGroup logContents = new GUILayoutGroup(new RectTransform(Vector2.One, logFrame.RectTransform, Anchor.Center)) { Stretch = true }; GUILayoutGroup socialHolder = null; GUILayoutGroup serverLogHolder = null; - LogButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), logHolder.RectTransform), true) + LogButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), logContents.RectTransform), true) { Stretch = true, RelativeSpacing = 0.02f @@ -447,7 +1247,7 @@ namespace Barotrauma clientHiddenElements.Add(LogButtons); // Show chat button - showChatButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), + chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), TextManager.Get("Chat"), style: "GUITabButton") { Selected = true, @@ -455,34 +1255,32 @@ namespace Barotrauma { if (socialHolder != null) { socialHolder.Visible = true; } if (serverLogHolder != null) { serverLogHolder.Visible = false; } - showChatButton.Selected = true; - showLogButton.Selected = false; + chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button); return true; } - }; + }); // Server log button - showLogButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), + chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), TextManager.Get("ServerLog"), style: "GUITabButton") { OnClicked = (GUIButton button, object userData) => { if (socialHolder != null) { socialHolder.Visible = false; } - if (!(serverLogHolder?.Visible ?? true)) + if (serverLogHolder is { Visible: false }) { if (GameMain.Client?.ServerSettings?.ServerLog == null) { return false; } serverLogHolder.Visible = true; GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogReverseButton, serverLogBox, serverLogFilterTicks.Content, serverLogFilter); } - showChatButton.Selected = false; - showLogButton.Selected = true; + chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button); return true; } - }; + }); - GUITextBlock.AutoScaleAndNormalize(showChatButton.TextBlock, showLogButton.TextBlock); + GUITextBlock.AutoScaleAndNormalize(chatPanelTabButtons.Select(btn => btn.TextBlock)); - GUIFrame logHolderBottom = new GUIFrame(new RectTransform(Vector2.One, logHolder.RectTransform), style: null) + GUIFrame logHolderBottom = new GUIFrame(new RectTransform(Vector2.One, logContents.RectTransform), style: null) { CanBeFocused = false }; @@ -586,6 +1384,65 @@ namespace Barotrauma Font = GUIStyle.SmallFont }; + } + + private void CreateBottomPanelContents(GUIComponent bottomBar) + { + //bottom panel ------------------------------------------------------------ + + GUILayoutGroup bottomBarLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + GUILayoutGroup bottomBarMid = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + GUILayoutGroup bottomBarRight = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = PanelSpacing + }; + + var disconnectButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect")) + { + OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; } + }; + disconnectButton.TextBlock.AutoScaleHorizontal = true; + + + // file transfers ------------------------------------------------------------ + FileTransferFrame = new GUIFrame(new RectTransform(Vector2.One, bottomBarLeft.RectTransform), style: "TextFrame"); + var fileTransferContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), FileTransferFrame.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont); + var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); + FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", + font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); + new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") + { + OnClicked = (btn, userdata) => + { + if (FileTransferFrame.UserData is not FileReceiver.FileTransferIn transfer) { return false; } + GameMain.Client?.CancelFileTransfer(transfer); + GameMain.Client?.FileReceiver.StopTransfer(transfer); + return true; + } + }; + + roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { @@ -601,9 +1458,8 @@ namespace Barotrauma ReadyToStartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), readyToStartContainer.RectTransform, anchor: Anchor.Center), TextManager.Get("ReadyToStartTickBox")); - // Spectate button - spectateButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), - TextManager.Get("SpectateButton")); + joinOnGoingRoundButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), + TextManager.Get("ServerListJoin")); // Start button StartButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), @@ -612,8 +1468,15 @@ namespace Barotrauma OnClicked = (btn, obj) => { if (GameMain.Client == null) { return true; } - GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); + if (CampaignSetupFrame.Visible && CampaignSetupUI != null) + { + CampaignSetupUI.StartGameClicked(btn, obj); + } + else + { + GameMain.Client.RequestStartRound(); + CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); + } return true; } }; @@ -629,676 +1492,18 @@ namespace Barotrauma { OnSelected = (tickBox) => { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; - clientDisabledElements.Add(autoRestartBoxContainer); + clientDisabledElements.Add(autoRestartBox); + AssignComponentToServerSetting(autoRestartBox, nameof(ServerSettings.AutoRestart)); - //-------------------------------------------------------------------------------------------------------------------------------- - //infoframe contents - //-------------------------------------------------------------------------------------------------------------------------------- - - //server info ------------------------------------------------------------------ - - // Server Info Header - GUILayoutGroup lobbyHeader = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), infoFrameContent.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - ServerName = new GUITextBox(new RectTransform(Vector2.One, lobbyHeader.RectTransform)) - { - MaxTextLength = NetConfig.ServerNameMaxLength, - OverflowClip = true - }; - ServerName.OnDeselected += (textBox, key) => - { - if (GameMain.Client == null) { return; } - if (!textBox.Readonly) - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Name); - } - }; - clientReadonlyElements.Add(ServerName); - - Favorite = new GUITickBox(new RectTransform(new Vector2(1.0f, 1.0f), lobbyHeader.RectTransform, scaleBasis: ScaleBasis.BothHeight), - "", null, "GUIServerListFavoriteTickBox") - { - Selected = false, - ToolTip = TextManager.Get("addtofavorites"), - OnSelected = (tickbox) => - { - if (GameMain.Client == null) { return true; } - ServerInfo info = GameMain.Client.CreateServerInfoFromSettings(); - if (tickbox.Selected) - { - GameMain.ServerListScreen.AddToFavoriteServers(info); - } - else - { - GameMain.ServerListScreen.RemoveFromFavoriteServers(info); - } - tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); - return true; - } - }; - - SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), lobbyHeader.RectTransform, Anchor.TopRight), - TextManager.Get("ServerSettingsButton")); - clientHiddenElements.Add(SettingsButton); - - lobbyHeader.RectTransform.MinSize = new Point(0, Math.Max(ServerName.Rect.Height, SettingsButton.Rect.Height)); - - GUILayoutGroup lobbyContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), infoFrameContent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.025f - }; - - GUILayoutGroup serverInfoHolder = new GUILayoutGroup(new RectTransform(Vector2.One, lobbyContent.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.025f - }; - - var serverBanner = new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.25f), serverInfoHolder.RectTransform), DrawServerBanner) - { - HideElementsOutsideFrame = true - }; - new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), serverBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.04f) }, - "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") - { - CanBeFocused = false - }; - - publicOrPrivate = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), serverBanner.RectTransform, Anchor.BottomRight, Pivot.BottomRight), - "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") - { - CanBeFocused = false - }; - - var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), serverInfoHolder.RectTransform)); - ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), - style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft); - var serverMessageHint = new GUITextBlock(new RectTransform(Vector2.One, ServerMessage.RectTransform), - textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, text: TextManager.Get("ClickToWriteServerMessage")); - - void updateServerMessageScrollBasedOnCaret() - { - float caretY = ServerMessage.CaretScreenPos.Y; - float bottomCaretExtent = ServerMessage.Font.LineHeight * 1.5f; - float topCaretExtent = -ServerMessage.Font.LineHeight * 0.5f; - if (caretY + bottomCaretExtent > serverMessageContainer.Rect.Bottom) - { - serverMessageContainer.ScrollBar.BarScroll - = (caretY - ServerMessage.Rect.Top - serverMessageContainer.Rect.Height + bottomCaretExtent) - / (ServerMessage.Rect.Height - serverMessageContainer.Rect.Height); - } - else if (caretY + topCaretExtent < serverMessageContainer.Rect.Top) - { - serverMessageContainer.ScrollBar.BarScroll - = (caretY - ServerMessage.Rect.Top + topCaretExtent) - / (ServerMessage.Rect.Height - serverMessageContainer.Rect.Height); - } - } - - ServerMessage.OnSelected += (textBox, key) => - { - serverMessageHint.Visible = false; - updateServerMessageScrollBasedOnCaret(); - }; - ServerMessage.OnTextChanged += (textBox, text) => - { - serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); - RefreshServerInfoSize(); - return true; - }; - ServerMessage.RectTransform.SizeChanged += RefreshServerInfoSize; - - void RefreshServerInfoSize() - { - serverMessageHint.Visible = !ServerMessage.Selected && !ServerMessage.Readonly && string.IsNullOrWhiteSpace(ServerMessage.Text); - Vector2 textSize = ServerMessage.Font.MeasureString(ServerMessage.WrappedText); - ServerMessage.RectTransform.NonScaledSize = new Point(ServerMessage.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10)); - serverMessageContainer.UpdateScrollBarSize(); - } - - ServerMessage.OnEnterPressed += (textBox, text) => - { - string str = textBox.Text; - int caretIndex = textBox.CaretIndex; - textBox.Text = $"{str[..caretIndex]}\n{str[caretIndex..]}"; - textBox.CaretIndex = caretIndex + 1; - - return true; - }; - ServerMessage.OnDeselected += (textBox, key) => - { - if (GameMain.Client == null) { return; } - if (!textBox.Readonly) - { - GameMain.Client?.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Message); - } - serverMessageHint.Visible = !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text); - }; - - ServerMessage.OnKeyHit += (sender, key) => updateServerMessageScrollBasedOnCaret(); - - - clientHiddenElements.Add(serverMessageHint); - clientReadonlyElements.Add(ServerMessage); - - //submarine list ------------------------------------------------------------------ - - GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(Vector2.One, lobbyContent.RectTransform)) - { - RelativeSpacing = panelSpacing, - Stretch = true - }; - - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUIStyle.SubHeadingFont); - - SubVisibilityButton - = new GUIButton( - new RectTransform(Vector2.One * 1.2f, subLabel.RectTransform, anchor: Anchor.CenterRight, - scaleBasis: ScaleBasis.BothHeight), - style: "EyeButton") - { - OnClicked = (button, o) => - { - CreateSubmarineVisibilityMenu(); - return false; - } - }; - clientHiddenElements.Add(SubVisibilityButton); - - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) - { - Stretch = true - }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); - subSearchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; - subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - subSearchBox.OnTextChanged += (textBox, text) => - { - UpdateSubVisibility(); - return true; - }; - - SubList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) - { - PlaySoundOnSelect = true, - OnSelected = VotableClicked - }; - - var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subLabel.RectTransform, Anchor.TopRight), - TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) - { - UserData = "subvotes", - Visible = false, - CanBeFocused = false - }; - - //respawn shuttle / submarine preview ------------------------------------------------------------------ - - GUILayoutGroup rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), lobbyContent.RectTransform)) - { - RelativeSpacing = panelSpacing, - Stretch = true - }; - - GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) - { - Stretch = true - }; - - shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) - { - Selected = true, - OnSelected = (GUITickBox box) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); - return true; - } - }; - shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => - { - shuttleTickBox.TextBlock.AutoScaleHorizontal = true; - shuttleTickBox.TextBlock.TextScale = 1.0f; - if (shuttleTickBox.TextBlock.TextScale < 0.75f) - { - shuttleTickBox.TextBlock.Wrap = true; - shuttleTickBox.TextBlock.AutoScaleHorizontal = true; - shuttleTickBox.TextBlock.TextScale = 1.0f; - } - }; - ShuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) - { - OnSelected = (component, obj) => - { - GameMain.Client?.RequestSelectSub(obj as SubmarineInfo, isShuttle: true); - return true; - } - }; - ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); - shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); - - subPreviewContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), rightColumn.RectTransform), style: null); - subPreviewContainer.RectTransform.SizeChanged += () => - { - if (SelectedSub != null) { CreateSubPreview(SelectedSub); } - }; - - //------------------------------------------------------------------------------------------------------------------ - // Gamemode panel - //------------------------------------------------------------------------------------------------------------------ - - GUILayoutGroup gameModeBackground = new GUILayoutGroup(new RectTransform(Vector2.One, gameModeContainer.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) - { - Stretch = true - }; - - var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), gameModeHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("GameMode"), font: GUIStyle.SubHeadingFont); - voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), - TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) - { - UserData = "modevotes", - Visible = false - }; - ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) - { - PlaySoundOnSelect = true, - OnSelected = VotableClicked - }; - - foreach (GameModePreset mode in GameModePreset.List) - { - if (mode.IsSinglePlayer) { continue; } - - var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), ModeList.Content.RectTransform), style: null) - { - UserData = mode - }; - - var modeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.9f), modeFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.02f, 0.0f) }) - { - AbsoluteSpacing = (int)(5 * GUI.Scale), - Stretch = true - }; - - var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); - var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); - //leave some padding for the vote count text - modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); - modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; - modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; - modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; - modeFrame.OnAddedToGUIUpdateList = (c) => - { - modeTitle.State = modeDescription.State = c.State; - }; - - new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, - style: "GameModeIcon." + mode.Identifier, scaleToFit: true); - - modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); - } - - var gameModeSpecificFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform), style: null); - CampaignSetupFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) - { - Visible = false - }; - CampaignFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) - { - Visible = false - }; - GUILayoutGroup campaignContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.5f), CampaignFrame.RectTransform, Anchor.Center)) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); - ContinueCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("campaigncontinue"), textAlignment: Alignment.Center) - { - OnClicked = (_, __) => - { - CoroutineManager.StartCoroutine(WaitForStartRound(ContinueCampaignButton), "WaitForStartRound"); - GameMain.Client?.RequestStartRound(true); - return true; - } - }; - QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("quitbutton"), textAlignment: Alignment.Center) - { - OnClicked = (_, __) => - { - if (GameMain.Client == null) { return false; } - if (GameMain.Client.GameStarted) - { - GameMain.Client.RequestRoundEnd(save: false); - } - else - { - GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true); - } - return true; - } - }; - - //mission type ------------------------------------------------------------------ - MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null); - - GUILayoutGroup missionHolder = new GUILayoutGroup(new RectTransform(Vector2.One, MissionTypeFrame.RectTransform)) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("MissionType"), font: GUIStyle.SubHeadingFont); - missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) - { - OnSelected = (component, obj) => - { - return false; - } - }; - - var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); - missionTypeTickBoxes = new GUITickBox[missionTypes.Length - 2]; - int index = 0; - for (int i = 0; i < missionTypes.Length; i++) - { - var missionType = missionTypes[i]; - if (missionType == MissionType.None || missionType == MissionType.All) { continue; } - - GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, style: "ListBoxElement") - { - UserData = missionType, - }; - - if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) - { - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), string.Empty) - { - UserData = (int)missionType, - Visible = false, - CanBeFocused = false - }; - } - else - { - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), - TextManager.Get("MissionType." + missionType.ToString())) - { - UserData = (int)missionType, - ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), - OnSelected = (tickbox) => - { - int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; - int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); - return true; - } - }; - frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; - } - index++; - } - clientDisabledElements.AddRange(missionTypeTickBoxes); - - //------------------------------------------------------------------ - // settings panel - //------------------------------------------------------------------ - - GUILayoutGroup settingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("Settings"), font: GUIStyle.SubHeadingFont); - - var settingsFrameTop = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), settingsHolder.RectTransform), style: "InnerFrame"); - var settingsContentTop = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), settingsFrameTop.RectTransform, Anchor.Center)) - { - Stretch = true, - AbsoluteSpacing = GUI.IntScale(10) - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 30) }, - TextManager.Get("TraitorSettings"), font: GUIStyle.SubHeadingFont); - - var settingsFrameBottom = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), settingsHolder.RectTransform), style: "InnerFrame"); - var settingsContentBottom = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.85f), settingsFrameBottom.RectTransform, Anchor.Center)) - { - Stretch = true, - AbsoluteSpacing = GUI.IntScale(10) - }; - - //seed ------------------------------------------------------------------ - - var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), TextManager.Get("LevelSeed")); - SeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); - SeedBox.OnDeselected += (textBox, key) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); - }; - clientDisabledElements.Add(SeedBox); - LevelSeed = ToolBox.RandomSeed(8); - - //level difficulty ------------------------------------------------------------------ - - var difficultyHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), settingsContentTop.RectTransform), style: null) - { - CanBeFocused = true - }; - - var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform), TextManager.Get("LevelDifficulty")) - { - ToolTip = TextManager.Get("leveldifficultyexplanation") - }; - - levelDifficultyScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.5f), difficultyHolder.RectTransform, Anchor.BottomCenter), style: "GUISlider", barSize: 0.2f) - { - Step = 0.01f, - Range = new Vector2(0.0f, 100.0f), - ToolTip = TextManager.Get("leveldifficultyexplanation"), - OnReleased = (scrollbar, value) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); - return true; - } - }; - var difficultyName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), "", textAlignment: Alignment.CenterRight) - { - ToolTip = TextManager.Get("leveldifficultyexplanation") - }; - levelDifficultyScrollBar.OnMoved = (scrollbar, value) => - { - if (!EventManagerSettings.Prefabs.Any()) { return true; } - difficultyName.Text = - EventManagerSettings.GetByDifficultyPercentile(value).Name - + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})"; - difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); - return true; - }; - - clientDisabledElements.Add(levelDifficultyScrollBar); - - //bot count ------------------------------------------------------------------ - - var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); - var botCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - botCountButtons = new GUIButton[2]; - botCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botCountContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: -1); - return true; - } - }; - - botCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); - botCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botCountContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: 1); - return true; - } - }; - botCountSettingHolder.RectTransform.MinSize = new Point(0, SeedBox.RectTransform.MinSize.Y); - clientDisabledElements.AddRange(botCountButtons); - - var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContentTop.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); - var botSpawnModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - botSpawnModeButtons = new GUIButton[2]; - botSpawnModeButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botSpawnModeContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: -1); - return true; - } - }; - - botSpawnModeText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botSpawnModeContainer.RectTransform), "", textAlignment: Alignment.Center, style: "GUITextBox"); - botSpawnModeButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botSpawnModeContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: 1); - return true; - } - }; - - clientDisabledElements.AddRange(botSpawnModeButtons); - - settingsBlocker = new GUIFrame(new RectTransform(Vector2.One, settingsFrameTop.RectTransform), style: "InnerFrame") - { - Color = Color.Black * 0.85f, - IgnoreLayoutGroups = true, - Visible = false - }; - new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.3f), settingsBlocker.RectTransform, Anchor.Center), - TextManager.Get("settings.campaigndisabled"), wrap: true, style: "InnerFrame", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - - //traitor probability ------------------------------------------------------------------ - - var traitorsProbHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), settingsContentBottom.RectTransform), style: null); - - var traitorProbLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform), TextManager.Get("traitor.probability")); - - traitorProbabilitySlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.5f), traitorsProbHolder.RectTransform, Anchor.BottomCenter), style: "GUISlider", barSize: 0.2f) - { - Step = 0.05f, - Range = new Vector2(0.0f, 1.0f), - ToolTip = TextManager.Get("traitor.probability.tooltip"), - OnReleased = (scrollbar, value) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorProbability: value); - return true; - } - }; - var traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), traitorProbLabel.RectTransform), "", textAlignment: Alignment.CenterRight) - { - ToolTip = TextManager.Get("traitor.probability.tooltip") - }; - traitorProbabilitySlider.OnMoved = (scrollbar, value) => - { - traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString()); - traitorProbabilityText.TextColor = - value <= 0.0f ? - GUIStyle.Green : - ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red); - return true; - }; - - traitorElements.Clear(); - traitorElements.Add(traitorProbabilityText); - traitorElements.Add(traitorProbabilitySlider); - clientDisabledElements.Add(traitorProbabilitySlider); - - var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), settingsContentBottom.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true) - { - ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip") - }; - - var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var traitorDangerButtons = new GUIButton[2]; - traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1); - return true; - } - }; - - traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - AbsoluteSpacing = 1 - }; - for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++) - { - var difficultyColor = Mission.GetDifficultyColor(i); - new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true) - { - ToolTip = - RichString.Rich( - $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' + - TextManager.Get($"traitor.dangerlevel.{i}.description")), - Color = difficultyColor, - DisabledColor = Color.Gray * 0.5f, - }; - } - - traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1); - return true; - } - }; - - SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel); - traitorElements.AddRange(traitorDangerButtons); - clientDisabledElements.AddRange(traitorDangerButtons); - - settingsContentTop.Recalculate(); - settingsContentBottom.Recalculate(); } public void StopWaitingForStartRound() { CoroutineManager.StopCoroutines("WaitForStartRound"); - if (StartButton != null) { StartButton.Enabled = true; @@ -1306,7 +1511,7 @@ namespace Barotrauma GUI.ClearCursorWait(); } - public IEnumerable WaitForStartRound(GUIButton startButton) + public static IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait"); @@ -1352,13 +1557,14 @@ namespace Barotrauma CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; - infoFrameContent.Recalculate(); - Character.Controlled = null; GameMain.LightManager.LosEnabled = false; GUI.PreventPauseMenuToggle = false; CampaignCharacterDiscarded = false; + changesPendingText?.Parent?.RemoveChild(changesPendingText); + changesPendingText = null; + chatInput.Select(); chatInput.OnEnterPressed = GameMain.Client.EnterChatMessage; chatInput.OnTextChanged += GameMain.Client.TypingChatMessage; @@ -1372,7 +1578,6 @@ namespace Barotrauma //disable/hide elements the clients are not supposed to use/see clientDisabledElements.ForEach(c => c.Enabled = false); - clientReadonlyElements.ForEach(c => c.Readonly = true); clientHiddenElements.ForEach(c => c.Visible = false); RefreshEnabledElements(); @@ -1380,86 +1585,104 @@ namespace Barotrauma if (GameMain.Client != null) { ChatManager.RegisterKeys(chatInput, GameMain.Client.ChatBox.ChatManager); - spectateButton.Visible = GameMain.Client.GameStarted; + joinOnGoingRoundButton.Visible = GameMain.Client.GameStarted; ReadyToStartBox.Selected = false; GameMain.Client.SetReadyToStart(ReadyToStartBox); } else { - spectateButton.Visible = false; + joinOnGoingRoundButton.Visible = false; } SetSpectate(spectateBox.Selected); if (GameMain.Client != null) { GameMain.Client.Voting.ResetVotes(GameMain.Client.ConnectedClients); - spectateButton.OnClicked = GameMain.Client.SpectateClicked; + joinOnGoingRoundButton.OnClicked = GameMain.Client.JoinOnGoingClicked; ReadyToStartBox.OnSelected = GameMain.Client.SetReadyToStart; } roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Recalculate(); - base.Select(); - } + AssignComponentsToServerSettings(); - public void SetPublic(bool isPublic) - { - publicOrPrivate.Text = isPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); + RefreshPlaystyleIcons(); + + base.Select(); } public void RefreshEnabledElements() { - bool manageSettings = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + if (GameMain.Client == null) { return; } + var client = GameMain.Client; + var settings = client.ServerSettings; + bool manageSettings = HasPermission(ClientPermissions.ManageSettings); + bool campaignSelected = CampaignFrame.Visible || CampaignSetupFrame.Visible; + bool campaignStarted = CampaignFrame.Visible; + bool gameStarted = client != null && client.GameStarted; - ServerName.Readonly = !manageSettings; - ServerMessage.Readonly = !manageSettings; - missionTypeList.Enabled = manageSettings; - foreach (var tickBox in missionTypeTickBoxes) + //disable elements the client doesn't have access to + foreach (var element in clientDisabledElements) { - tickBox.Enabled = manageSettings; + element.Enabled = manageSettings; } - SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; - levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && manageSettings; - levelDifficultyScrollBar.ToolTip = string.Empty; - if (!levelDifficultyScrollBar.Enabled) + traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0); + SetTraitorDangerIndicators(settings.TraitorDangerLevel); + respawnSettingsElements.ForEach(e => e.Enabled &= settings.AllowRespawn); + + //go through the individual elements that are only enabled in a specific context + if (ShuttleList != null) { - levelDifficultyScrollBar.ToolTip = TextManager.Get("campaigndifficultydisabled"); + ShuttleList.Enabled = ShuttleList.ButtonEnabled = HasPermission(ClientPermissions.SelectSub) && !gameStarted && settings.AllowRespawn; } + if (SubList != null) + { + SubList.Enabled = !campaignStarted && (settings.AllowSubVoting || HasPermission(ClientPermissions.SelectSub)); + } + if (ModeList != null) + { + ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode)); + } + shuttleTickBox.Enabled &= !gameStarted; - traitorElements.ForEach(e => e.Enabled = manageSettings); - botCountButtons[0].Enabled = botCountButtons[1].Enabled = manageSettings; - botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = manageSettings; + RefreshStartButtonVisibility(); - autoRestartBox.Enabled = manageSettings; + botSettingsElements.ForEach(b => b.Enabled = !campaignStarted && manageSettings); - SettingsButton.Visible = manageSettings; - SettingsButton.OnClicked = GameMain.Client.ServerSettings.ToggleSettingsFrame; - StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; - ServerName.Readonly = !manageSettings; - ServerMessage.Readonly = !manageSettings; - shuttleTickBox.Enabled = manageSettings && !GameMain.Client.GameStarted; - SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub) && !GameMain.Client.GameStarted; - ModeList.Enabled = !GameMain.Client.GameStarted && (GameMain.Client.ServerSettings.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode)); - LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); - GameMain.Client.UpdateLogButtonPermissions(); + campaignDisabledElements.ForEach(e => e.Enabled = !campaignSelected && manageSettings); + levelDifficultySlider.ToolTip = levelDifficultySlider.Enabled ? string.Empty : TextManager.Get("campaigndifficultydisabled"); + + //hide elements the client shouldn't + foreach (var element in clientHiddenElements) + { + element.Visible = manageSettings; + } + //go through the individual elements that are only visible in a specific context + ReadyToStartBox.Parent.Visible = !gameStarted; + LogButtons.Visible = HasPermission(ClientPermissions.ServerLog); + + client?.UpdateLogButtonPermissions(); roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); - SubVisibilityButton.Visible = manageSettings; - - ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; + SettingsButton.OnClicked = settings.ToggleSettingsFrame; RefreshGameModeContent(); + + static bool HasPermission(ClientPermissions permissions) + { + if (GameMain.Client == null) { return false; } + return GameMain.Client.HasPermission(permissions); + } } public void ShowSpectateButton() { if (GameMain.Client == null) { return; } - spectateButton.Visible = true; - spectateButton.Enabled = true; + joinOnGoingRoundButton.Visible = true; + joinOnGoingRoundButton.Enabled = true; StartButton.Visible = false; } @@ -1483,14 +1706,14 @@ namespace Barotrauma private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing = true) { - UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContainer); + UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContent); } public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { if (GameMain.Client == null) { return; } UpdatePlayerFrame( - Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, + Character.Controlled?.Info ?? playerInfoContent.UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, parent: parent, createPendingText: createPendingText); @@ -1512,12 +1735,8 @@ namespace Barotrauma bool isGameRunning = GameMain.GameSession?.IsRunning ?? false; - infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.97f : 0.92f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) - { - RelativeSpacing = 0.0f, - Stretch = true, - UserData = characterInfo - }; + parent.ClearChildren(); + parent.UserData = characterInfo; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; changesPendingText?.Parent?.RemoveChild(changesPendingText); @@ -1528,8 +1747,7 @@ namespace Barotrauma CreateChangesPendingText(); } - - CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) + CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), parent.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, OverflowClip = true @@ -1566,11 +1784,11 @@ namespace Barotrauma }; //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), infoContainer.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), parent.RectTransform), style: null); if (allowEditing) { - GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.016f), infoContainer.RectTransform), isHorizontal: true) + GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), parent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f @@ -1592,7 +1810,7 @@ namespace Barotrauma // Unsubscribe from previous events, not even sure if this matters here but it doesn't hurt so why not if (characterInfoFrame != null) { characterInfoFrame.RectTransform.SizeChanged -= RecalculateSubDescription; } - characterInfoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), infoContainer.RectTransform), style: null); + characterInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); characterInfoFrame.RectTransform.SizeChanged += RecalculateSubDescription; JobPreferenceContainer = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), @@ -1604,7 +1822,7 @@ namespace Barotrauma PlaySoundOnSelect = true, OnSelected = (child, obj) => { - if (child.IsParentOf(GUI.MouseOn)) return false; + if (child.IsParentOf(GUI.MouseOn)) { return false; } return OpenJobSelection(child, obj); } }; @@ -1622,7 +1840,7 @@ namespace Barotrauma } // The old job variant system used one-based indexing // so let's make sure no one get to pick a variant which doesn't exist - var variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); + int variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); jobPrefab = new JobVariant(prefab, variant); break; } @@ -1644,28 +1862,28 @@ namespace Barotrauma } else { - characterInfo.CreateIcon(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter)); + characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.16f), parent.RectTransform, Anchor.TopCenter)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, SelectedColor = Color.Transparent }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); foreach (Skill skill in characterInfo.Job.GetSkills()) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); - var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), + var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), textColor, font: GUIStyle.SmallFont); } // Spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), infoContainer.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), parent.RectTransform), style: null); - new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), infoContainer.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew")) + new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew")) { IgnoreLayoutGroups = true, OnClicked = (btn, userdata) => @@ -1682,7 +1900,7 @@ namespace Barotrauma TeamPreferenceListBox = null; if (SelectedMode == GameModePreset.PvP) { - TeamPreferenceListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.04f), infoContainer.RectTransform, anchor: Anchor.TopLeft, pivot: Pivot.TopLeft), isHorizontal: true, style: null) + TeamPreferenceListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.04f), parent.RectTransform, anchor: Anchor.TopLeft, pivot: Pivot.TopLeft), isHorizontal: true, style: null) { Enabled = true, KeepSpaceForScrollBar = false, @@ -1690,7 +1908,7 @@ namespace Barotrauma ScrollBarEnabled = false, ScrollBarVisible = false }; - + TeamPreferenceListBox.RectTransform.MinSize = new Point(0, GUI.IntScale(30)); TeamPreferenceListBox.UpdateDimensions(); Color team1Color = new Color(0, 110, 150, 255); @@ -1770,9 +1988,9 @@ namespace Barotrauma private void CreateChangesPendingText() { - if (!createPendingChangesText || changesPendingText != null || infoContainer == null) { return; } + if (!createPendingChangesText || changesPendingText != null || playerInfoContent == null) { return; } - changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.Parent.Parent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) }, + changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), playerInfoContent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) }, style: "OuterGlow") { Color = Color.Black, @@ -1834,7 +2052,7 @@ namespace Barotrauma int i = 0; foreach (var child in traitorDangerGroup.Children) { - child.Enabled = i < dangerLevel; + child.Enabled = i < dangerLevel && GameMain.Client?.ServerSettings is { TraitorProbability: > 0 }; i++; } } @@ -1848,16 +2066,16 @@ namespace Barotrauma public void SetSpectate(bool spectate) { if (GameMain.Client == null) { return; } - this.spectateBox.Selected = spectate; + spectateBox.Selected = spectate; if (spectate) { - playerInfoContainer.ClearChildren(); + playerInfoContent.ClearChildren(); GameMain.Client.CharacterInfo?.Remove(); GameMain.Client.CharacterInfo = null; GameMain.Client.Character?.Remove(); GameMain.Client.Character = null; - new GUITextBlock(new RectTransform(Vector2.One, playerInfoContainer.RectTransform, Anchor.Center), + new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center), TextManager.Get("PlayingAsSpectator"), textAlignment: Alignment.Center); } @@ -1877,10 +2095,6 @@ namespace Barotrauma // Hide spectate tickbox if spectating is not allowed spectateBox.Visible = allowSpectating; - if (infoContainer != null) - { - infoContainer.RectTransform.RelativeSize = new Vector2(infoContainer.RectTransform.RelativeSize.X, spectateBox.Visible ? 0.92f : 0.97f); - } } public void SetAutoRestart(bool enabled, float timer = 0.0f) @@ -1929,14 +2143,15 @@ namespace Barotrauma }; int buttonSize = (int)(frame.Rect.Height * 0.8f); - var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft) /*{ AbsoluteOffset = new Point(buttonSize + 5, 0) }*/, + var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) { CanBeFocused = false }; - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation); - if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); + var matchingSub = + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation) ?? + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); if (matchingSub == null) { @@ -2437,12 +2652,19 @@ namespace Barotrauma RelativeSpacing = 0.03f }; - var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, hasManagePermissions ? 0.1f : 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: false); + + var headerTextContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), + var headerVolumeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerTextContainer.RectTransform), text: selectedClient.Name, font: GUIStyle.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); @@ -2615,7 +2837,7 @@ namespace Barotrauma rankDropDown.SelectItem(null); DebugConsole.Command selectedCommand = tickBox.UserData as DebugConsole.Command; - if (!(PlayerFrame.UserData is Client client)) { return false; } + if (PlayerFrame.UserData is not Client client) { return false; } if (!tickBox.Selected) { @@ -2647,18 +2869,18 @@ namespace Barotrauma banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("clientpermission.unban")) { - UserData = selectedClient + UserData = selectedClient, + OnClicked = (bt, userdata) => { GameMain.Client?.UnbanPlayer(selectedClient.Name); return true; } }; - banButton.OnClicked = (bt, userdata) => { GameMain.Client?.UnbanPlayer(selectedClient.Name); return true; }; } else { banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("Ban")) { - UserData = selectedClient + UserData = selectedClient, + OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; } }; - banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; } banButton.OnClicked += ClosePlayerFrame; } @@ -2682,13 +2904,31 @@ namespace Barotrauma var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("Kick")) { - UserData = selectedClient + UserData = selectedClient, + OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; } }; - kickButton.OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; }; kickButton.OnClicked += ClosePlayerFrame; } - new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerContainer.RectTransform, Anchor.TopRight), + var volumeLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), headerVolumeContainer.RectTransform), isHorizontal: false); + var volumeTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get("VoiceChatVolume")); + var volumePercentageText = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(selectedClient.VoiceVolume), textAlignment: Alignment.Right); + new GUIScrollBar(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style: "GUISlider") + { + Range = new Vector2(0f, 1f), + BarScroll = selectedClient.VoiceVolume / Client.MaxVoiceChatBoost, + OnMoved = (_, barScroll) => + { + float newVolume = barScroll * Client.MaxVoiceChatBoost; + + selectedClient.VoiceVolume = newVolume; + volumePercentageText.Text = ToolBox.GetFormattedPercentage(newVolume); + return true; + } + }; + + new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerVolumeContainer.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { Selected = selectedClient.MutedLocally, @@ -2704,7 +2944,7 @@ namespace Barotrauma if (selectedClient.AccountId.TryUnwrap(out var accountId)) { - var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, + var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerTextContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, accountId.ViewProfileLabel()) { UserData = selectedClient @@ -2761,13 +3001,13 @@ namespace Barotrauma return true; } - public void KickPlayer(Client client) + public static void KickPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, false); } - public void BanPlayer(Client client) + public static void BanPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true); @@ -2837,7 +3077,7 @@ namespace Barotrauma if (GUI.MouseOn?.UserData is JobVariant jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") { - if (!(jobVariantTooltip?.UserData is JobVariant prevVisibleVariant) || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) + if (jobVariantTooltip?.UserData is not JobVariant prevVisibleVariant || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) { CreateJobVariantTooltip(jobPrefab.Prefab, jobPrefab.Variant, GUI.MouseOn.Parent); } @@ -2890,6 +3130,8 @@ namespace Barotrauma } private PlayStyle? prevPlayStyle = null; + private bool? prevIsPublic = null; + private void DrawServerBanner(SpriteBatch spriteBatch, GUICustomComponent component) { if (GameMain.NetworkMember?.ServerSettings == null) { return; } @@ -2901,21 +3143,24 @@ namespace Barotrauma .GetSprite(GUIComponent.ComponentState.None); if (sprite is null) { return; } - float scale = component.Rect.Width / sprite.size.X; - sprite.Draw(spriteBatch, component.Center, scale: scale); + GUI.DrawBackgroundSprite(spriteBatch, sprite, Color.White, drawArea: component.Rect); if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value) { - var nameText = component.GetChild(); - nameText.Text = TextManager.Get($"ServerTag.{playStyle}"); - nameText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White; - nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); + playstyleText.Text = TextManager.Get($"ServerTag.{playStyle}"); + playstyleText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White; + playstyleText.RectTransform.NonScaledSize = (playstyleText.Font.MeasureString(playstyleText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); prevPlayStyle = playStyle; - - component.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}"); + (playstyleText.Parent as GUILayoutGroup)?.Recalculate(); + playstyleText.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}"); + } + if (!prevIsPublic.HasValue || GameMain.NetworkMember.ServerSettings.IsPublic != prevIsPublic.Value) + { + publicOrPrivateText.Text = GameMain.NetworkMember.ServerSettings.IsPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); + publicOrPrivateText.RectTransform.NonScaledSize = (publicOrPrivateText.Font.MeasureString(publicOrPrivateText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); + (publicOrPrivateText.Parent as GUILayoutGroup)?.Recalculate(); + prevIsPublic = GameMain.NetworkMember.ServerSettings.IsPublic; } - - publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); } private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) @@ -3097,8 +3342,7 @@ namespace Barotrauma bool moveToNext = obj != null; var jobPrefab = (obj as JobVariant)?.Prefab; - - var prevObj = child.UserData; + object prevObj = child.UserData; var existingChild = JobList.Content.FindChild(d => (d.UserData is JobVariant prefab) && (prefab.Prefab == jobPrefab)); if (existingChild != null && obj != null) @@ -3392,7 +3636,6 @@ namespace Barotrauma }); autoRestartBox.Parent.Visible = true; - settingsBlocker.Visible = false; if (SelectedMode == GameModePreset.Mission || SelectedMode == GameModePreset.PvP) { MissionTypeFrame.Visible = true; @@ -3406,9 +3649,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map != null) { //campaign running - settingsBlocker.Visible = true; CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound); - ContinueCampaignButton.Enabled = !GameMain.Client.GameStarted && CampaignFrame.Visible; CampaignSetupFrame.Visible = false; } else @@ -3457,12 +3698,27 @@ namespace Barotrauma } ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; + RefreshStartButtonVisibility(); + } - StartButton.Visible = - GameMain.Client.HasPermission(ClientPermissions.ManageRound) && - !GameMain.Client.GameStarted && - !CampaignSetupFrame.Visible && - !CampaignFrame.Visible; + public void RefreshStartButtonVisibility() + { + if (CampaignSetupUI != null && CampaignSetupFrame is { Visible: true }) + { + //setting up a campaign -> start button only visible if we're in the "new game" tab (load game menu not visible) + StartButton.Visible = + !GameMain.Client.GameStarted && + !CampaignSetupUI.LoadGameMenuVisible && + (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); + } + else + { + //if a campaign is currently running, we must show the start button to allow continuing + bool campaignActive = GameMain.GameSession?.GameMode is CampaignMode; + StartButton.Visible = + (SelectedMode != GameModePreset.MultiPlayerCampaign || campaignActive) && + !GameMain.Client.GameStarted && GameMain.Client.HasPermission(ClientPermissions.ManageRound); + } } public void ToggleCampaignMode(bool enabled) @@ -3472,16 +3728,12 @@ namespace Barotrauma //remove campaign character from the panel if (campaignCharacterInfo != null) { + campaignCharacterInfo = null; UpdatePlayerFrame(null); SetSpectate(spectateBox.Selected); } - campaignCharacterInfo = null; CampaignCharacterDiscarded = false; } - else - { - CampaignFrame.Visible = CampaignSetupFrame.Visible = false; - } RefreshEnabledElements(); if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign) { @@ -3498,7 +3750,7 @@ namespace Barotrauma subPreviewContainer.ClearChildren(); foreach (GUIComponent child in SubList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { continue; } + if (child.UserData is not SubmarineInfo sub) { continue; } //just check the name, even though the campaign sub may not be the exact same version //we're selecting the sub just for show, the selection is not actually used for anything if (sub.Name == name) @@ -3676,12 +3928,24 @@ namespace Barotrauma private static bool StringsEqual(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); - + public static bool operator ==(FailedSubInfo a, FailedSubInfo b) => StringsEqual(a.Name, b.Name) && StringsEqual(a.Hash, b.Hash); public static bool operator !=(FailedSubInfo a, FailedSubInfo b) => !(a == b); + + public override int GetHashCode() + { + return HashCode.Combine(Name, Hash); + } + + public override bool Equals(object obj) + { + return obj is FailedSubInfo info && + Name == info.Name && + Hash == info.Hash; + } } public FailedSubInfo? FailedSelectedSub; @@ -3708,7 +3972,7 @@ namespace Barotrauma //matching sub found and already selected, all good if (sub != null) { - if (subList == this.SubList) + if (subList == SubList) { CreateSubPreview(sub); } @@ -3924,7 +4188,7 @@ namespace Barotrauma to.DraggedElement = draggedElement; - to.BarScroll = to.BarScroll * (oldCount / newCount); + to.BarScroll *= (oldCount / newCount); } } @@ -4095,7 +4359,7 @@ namespace Barotrauma { foreach (GUIComponent child in SubList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { continue; } + if (child.UserData is not SubmarineInfo sub) { continue; } child.Visible = (!GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name) || (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.Name.Equals(sub.Name, StringComparison.OrdinalIgnoreCase))) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index a435e37a7..472a0292d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -306,22 +306,6 @@ namespace Barotrauma OverflowClip = true }; - if (string.IsNullOrEmpty(ClientNameBox.Text)) - { - TaskPool.Add("GetDefaultUserName", - GetDefaultUserName(), - t => - { - if (!t.TryGetResult(out string name)) { return; } - if (ClientNameBox.Text.IsNullOrEmpty()) { ClientNameBox.Text = name; } - }); - } - ClientNameBox.OnTextChanged += (textbox, text) => - { - MultiplayerPreferences.Instance.PlayerName = text; - return true; - }; - var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); tabs[TabEnum.All] = new Tab(TabEnum.All, this, tabButtonHolder, ""); @@ -874,11 +858,44 @@ namespace Barotrauma return 0; } } - + public override void Select() { base.Select(); + + if (string.IsNullOrEmpty(ClientNameBox.Text)) + { + TaskPool.Add("GetDefaultUserName", + GetDefaultUserName(), + t => + { + if (!t.TryGetResult(out string name)) { return; } + if (ClientNameBox.Text.IsNullOrEmpty()) + { + ClientNameBox.Text = name; + string nameWithoutInvisibleSymbols = string.Empty; + foreach (char c in ClientNameBox.Text) + { + Vector2 size = ClientNameBox.Font.MeasureChar(c); + if (size.X > 0 && size.Y > 0) + { + nameWithoutInvisibleSymbols += c; + } + } + if (nameWithoutInvisibleSymbols != ClientNameBox.Text) + { + MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text = nameWithoutInvisibleSymbols; + new GUIMessageBox(TextManager.Get("Warning"), TextManager.GetWithVariable("NameContainsInvisibleSymbols", "[name]", nameWithoutInvisibleSymbols)); + } + } + }); + } + ClientNameBox.OnTextChanged += (textbox, text) => + { + MultiplayerPreferences.Instance.PlayerName = text; + return true; + }; if (EosInterface.IdQueries.IsLoggedIntoEosConnect) { if (SteamManager.IsInitialized) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index f32050a63..ae8b70f03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -50,6 +50,8 @@ namespace Barotrauma private bool editBackgroundColor; private Color backgroundColor = new Color(0.051f, 0.149f, 0.271f, 1.0f); + private bool ControlDown => PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl); + private readonly Camera cam; public override Camera Cam { @@ -366,7 +368,8 @@ namespace Barotrauma "DecorativeSprite", "BarrelSprite", "RailSprite", - "SchematicSprite" + "SchematicSprite", + "WeldedSprite" }; foreach (string spriteElementName in spriteElementNames) @@ -471,10 +474,10 @@ namespace Barotrauma public override void Update(double deltaTime) { base.Update(deltaTime); - Widget.EnableMultiSelect = PlayerInput.KeyDown(Keys.LeftControl); + Widget.EnableMultiSelect = ControlDown; spriteList.SelectMultiple = Widget.EnableMultiSelect; // Select rects with the mouse - if (Widget.selectedWidgets.None() || Widget.EnableMultiSelect) + if (Widget.SelectedWidgets.None() || Widget.EnableMultiSelect) { if (SelectedTexture != null && GUI.MouseOn == null) { @@ -578,7 +581,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Width--; } @@ -593,7 +596,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Width++; } @@ -608,7 +611,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Height++; } @@ -623,7 +626,7 @@ namespace Barotrauma foreach (var sprite in selectedSprites) { var newRect = sprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (ControlDown) { newRect.Height--; } @@ -637,6 +640,7 @@ namespace Barotrauma } } + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { graphics.Clear(backgroundColor); @@ -691,22 +695,22 @@ namespace Barotrauma Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2(); Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y); Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height); - var originWidget = GetWidget($"{id}_origin", sprite, widgetSize, Widget.Shape.Cross, initMethod: w => + var originWidget = GetWidget($"{id}_origin", sprite, widgetSize, WidgetShape.Cross, initMethod: w => { - w.tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); + w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition.Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom); sprite.Origin = (w.DrawPos - textureRect.Location.ToVector2() - sprite.SourceRect.Location.ToVector2() * zoom) / zoom; - w.tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); + w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal()); }; - w.refresh = () => + w.Refresh = () => w.DrawPos = (textureRect.Location.ToVector2() + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * zoom) .Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom); }); - var positionWidget = GetWidget($"{id}_position", sprite, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var positionWidget = GetWidget($"{id}_position", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.tooltip = positionLabel + sprite.SourceRect.Location; + w.Tooltip = positionLabel + sprite.SourceRect.Location; w.MouseHeld += dTime => { w.DrawPos = (drawGrid && snapToGrid) ? @@ -719,13 +723,13 @@ namespace Barotrauma // TODO: cache the sprite name? textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect; } - w.tooltip = positionLabel + sprite.SourceRect.Location; + w.Tooltip = positionLabel + sprite.SourceRect.Location; }; - w.refresh = () => w.DrawPos = textureRect.Location.ToVector2() + sprite.SourceRect.Location.ToVector2() * zoom; + w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + sprite.SourceRect.Location.ToVector2() * zoom; }); - var sizeWidget = GetWidget($"{id}_size", sprite, widgetSize, Widget.Shape.Rectangle, initMethod: w => + var sizeWidget = GetWidget($"{id}_size", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w => { - w.tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); + w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); w.MouseHeld += dTime => { w.DrawPos = (drawGrid && snapToGrid) ? @@ -740,9 +744,9 @@ namespace Barotrauma // TODO: cache the sprite name? textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect; } - w.tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); + w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString()); }; - w.refresh = () => w.DrawPos = textureRect.Location.ToVector2() + new Vector2(sprite.SourceRect.Right, sprite.SourceRect.Bottom) * zoom; + w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + new Vector2(sprite.SourceRect.Right, sprite.SourceRect.Bottom) * zoom; }); originWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null; positionWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null; @@ -1027,31 +1031,31 @@ namespace Barotrauma #region Widgets private Dictionary widgets = new Dictionary(); - private Widget GetWidget(string id, Sprite sprite, int size = 5, Widget.Shape shape = Widget.Shape.Rectangle, Action initMethod = null) + private Widget GetWidget(string id, Sprite sprite, int size = 5, WidgetShape shape = WidgetShape.Rectangle, Action initMethod = null) { if (!widgets.TryGetValue(id, out Widget widget)) { int selectedSize = (int)Math.Round(size * 1.5f); widget = new Widget(id, size, shape) { - data = sprite, - color = Color.Yellow, - secondaryColor = Color.Gray, - tooltipOffset = new Vector2(selectedSize / 2 + 5, -10) + Data = sprite, + Color = Color.Yellow, + SecondaryColor = Color.Gray, + TooltipOffset = new Vector2(selectedSize / 2 + 5, -10) }; widget.PreDraw += (sp, dTime) => { if (!widget.IsControlled) { - widget.refresh(); + widget.Refresh(); } }; widget.PreUpdate += dTime => widget.Enabled = selectedSprites.Contains(sprite); widget.PostUpdate += dTime => { - widget.inputAreaMargin = widget.IsControlled ? 1000 : 0; - widget.size = widget.IsSelected ? selectedSize : size; - widget.isFilled = widget.IsControlled; + widget.InputAreaMargin = widget.IsControlled ? 1000 : 0; + widget.Size = widget.IsSelected ? selectedSize : size; + widget.IsFilled = widget.IsControlled; }; widgets.Add(id, widget); initMethod?.Invoke(widget); @@ -1062,7 +1066,7 @@ namespace Barotrauma private void ResetWidgets() { widgets.Clear(); - Widget.selectedWidgets.Clear(); + Widget.SelectedWidgets.Clear(); } #endregion } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index e88263087..f67b89392 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; using Barotrauma.Steam; @@ -81,7 +81,10 @@ namespace Barotrauma ItemCount, LightCount, ShadowCastingLightCount, - WaterInHulls + WaterInHulls, + LowOxygenOutputWarning, + TooLargeForEndGame, + NotEnoughContainers } public static Vector2 MouseDragStart = Vector2.Zero; @@ -143,6 +146,7 @@ namespace Barotrauma private GUIButton visibilityButton; private GUIFrame layerPanel; private GUIListBox layerList; + private List layerSpecificButtons = new List(); private GUIFrame undoBufferPanel; private GUIFrame undoBufferDisclaimer; @@ -552,6 +556,21 @@ namespace Barotrauma } }; + spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null); + + var selectedLayerText = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), paddedTopPanel.RectTransform), + string.Empty, textAlignment: Alignment.Center); + selectedLayerText.TextGetter = () => + { + string selectedLayer = layerList.SelectedData as string; + if (selectedLayer != prevSelectedLayer) + { + prevSelectedLayer = selectedLayer; + return selectedLayer.IsNullOrEmpty() ? string.Empty : TextManager.GetWithVariable("editor.layer.editinglayer", "[layer]", selectedLayer); + } + return selectedLayerText.Text; + }; + TopPanel.RectTransform.MinSize = new Point(0, (int)(paddedTopPanel.RectTransform.Children.Max(c => c.MinSize.Y) / paddedTopPanel.RectTransform.RelativeSize.Y)); paddedTopPanel.Recalculate(); @@ -581,25 +600,27 @@ namespace Barotrauma { ScrollBarVisible = true, AutoHideScrollBar = false, - OnSelected = (component, o) => + OnSelected = (component, userdata) => { - if (GUI.MouseOn is GUITickBox) { return false; } // lol - if (!(o is string layer)) { return false; } - - MapEntity.SelectedList.Clear(); - foreach (MapEntity entity in MapEntity.MapEntityList.Where(me => !me.Removed && me.Layer == layer)) + //toggling selection is not how listboxes normally work, need to do that manually here + SoundPlayer.PlayUISound(GUISoundType.Select); + if (layerList.SelectedData == userdata) { - if (entity.IsSelected) { continue; } - - MapEntity.SelectedList.Add(entity); + layerSpecificButtons.ForEach(btn => btn.Enabled = false); + layerList.Deselect(); + return false; + } + else + { + layerSpecificButtons.ForEach(btn => btn.Enabled = true); + return true; } - return true; } }; GUILayoutGroup layerButtonGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), layerGroup.RectTransform)); - GUILayoutGroup layerButtonTopGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true); + GUILayoutGroup layerButtonBottomGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true); GUIButton layerAddButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.newlayer"), style: "GUIButtonFreeScale") { @@ -612,6 +633,7 @@ namespace Barotrauma GUIButton layerDeleteButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.deletelayer"), style: "GUIButtonFreeScale") { + Enabled = false, OnClicked = (button, o) => { if (layerList.SelectedData is string layer) @@ -620,10 +642,12 @@ namespace Barotrauma } return true; } - }; + }; + layerSpecificButtons.Add(layerDeleteButton); - GUIButton layerRenameButton = new GUIButton(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), text: TextManager.Get("editor.layer.renamelayer"), style: "GUIButtonFreeScale") + GUIButton layerRenameButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), layerButtonBottomGroup.RectTransform), text: TextManager.Get("editor.layer.renamelayer"), style: "GUIButtonFreeScale") { + Enabled = false, OnClicked = (button, o) => { if (layerList.SelectedData is string layer) @@ -636,9 +660,27 @@ namespace Barotrauma return true; } }; + layerSpecificButtons.Add(layerRenameButton); - GUITextBlock.AutoScaleAndNormalize(layerAddButton.TextBlock, layerDeleteButton.TextBlock, layerRenameButton.TextBlock); + GUIButton selectLayerButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), layerButtonBottomGroup.RectTransform), text: TextManager.Get("editor.layer.selectlayer"), style: "GUIButtonFreeScale") + { + Enabled = false, + OnClicked = (button, o) => + { + if (layerList.SelectedData is string layer) + { + foreach (MapEntity entity in MapEntity.MapEntityList.Where(me => !me.Removed && me.Layer == layer)) + { + if (entity.IsSelected) { continue; } + MapEntity.SelectedList.Add(entity); + } + } + return true; + } + }; + layerSpecificButtons.Add(selectLayerButton); + GUITextBlock.AutoScaleAndNormalize(layerAddButton.TextBlock, layerDeleteButton.TextBlock, layerRenameButton.TextBlock, selectLayerButton.TextBlock); Vector2 subPanelSize = new Vector2(0.925f, 0.9f); @@ -1214,17 +1256,13 @@ namespace Barotrauma frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); LocalizedString name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{name}‖color:end‖"; - if (!ep.Description.IsNullOrEmpty()) - { - frame.ToolTip += '\n' + ep.Description; - } + frame.ToolTip = ep.CreateTooltipText(); - if (ep.ContentPackage != GameMain.VanillaContent && ep.ContentPackage != null) + if (ep.IsModded) { frame.Color = Color.Magenta; - frame.ToolTip = $"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"; } + if (ep.HideInMenus || ep.HideInEditors) { frame.Color = Color.Red; @@ -1395,14 +1433,17 @@ namespace Barotrauma Level.Loaded?.GenerationParams?.AmbientLightColor ?? new Color(3, 3, 3, 3); - UpdateEntityList(); - isAutoSaving = false; + if (!wasSelectedBefore) { OpenEntityMenu(null); wasSelectedBefore = true; } + else + { + OpenEntityMenu(selectedCategory); + } if (backedUpSubInfo != null) { @@ -2038,11 +2079,9 @@ namespace Barotrauma saveFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.6f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.65f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; - //var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveSubDialogHeader"), font: GUIStyle.LargeFont); - var columnArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), paddedSaveFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.55f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.01f, Stretch = true }; var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.42f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.02f, Stretch = true }; @@ -2126,6 +2165,41 @@ namespace Barotrauma subTypeDropdown.AddItem(TextManager.Get(textTag), subType); } + if (Layers.Any()) + { + var layerVisibilityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), TextManager.Get("editor.layer.visiblebydefault"), textAlignment: Alignment.CenterLeft); + var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), + text: LocalizedString.Join(", ", Layers.Where(l => !Submarine.MainSub?.Info?.LayersHiddenByDefault?.Contains(l.ToIdentifier()) ?? false).Select(lt => TextManager.Capitalize(lt.Key)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); + foreach (string layerName in Layers.Keys) + { + layerVisibilityDropDown.AddItem(TextManager.Capitalize(layerName), layerName); + if (MainSub?.Info == null) { continue; } + if (!MainSub.Info.LayersHiddenByDefault.Contains(layerName.ToIdentifier())) + { + layerVisibilityDropDown.SelectItem(layerName); + } + } + layerVisibilityDropDown.OnSelected += (_, __) => + { + if (MainSub.Info == null) { return false; } + MainSub.Info.LayersHiddenByDefault.Clear(); + foreach (string layerName in Layers.Keys) + { + //selected as visible = not hidden + if (layerVisibilityDropDown.SelectedDataMultiple.Any(o => o as string == layerName)) + { + continue; + } + MainSub.Info.LayersHiddenByDefault.Add(layerName.ToIdentifier()); + } + + layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width); + return true; + }; + layerVisibilityGroup.RectTransform.MinSize = layerVisibilityDropDown.RectTransform.MinSize = new Point(0, layerVisibilityDropDown.RectTransform.Children.Max(c => c.MinSize.Y)); + } + //--------------------------------------- var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.5f), leftColumn.RectTransform), style: "InnerFrame"); @@ -2157,7 +2231,7 @@ namespace Barotrauma var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); - foreach (Identifier flag in availableFlags) + foreach (Identifier flag in availableFlags.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase)) { moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } @@ -2192,7 +2266,7 @@ namespace Barotrauma { allowAttachDropDown.SelectItem("any".ToIdentifier()); } - foreach (Identifier flag in availableFlags) + foreach (Identifier flag in availableFlags.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase)) { if (flag == "any" || flag == "none") { continue; } allowAttachDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); @@ -2218,12 +2292,13 @@ namespace Barotrauma var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any".ToIdentifier() }; + HashSet availableLocationTypes = new HashSet(); foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true); - foreach (Identifier locationType in availableLocationTypes) + locationTypeDropDown.AddItem(TextManager.Capitalize("any"), "any".ToIdentifier()); + foreach (Identifier locationType in availableLocationTypes.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase)) { locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType); if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } @@ -2242,7 +2317,6 @@ namespace Barotrauma }; locationTypeGroup.RectTransform.MinSize = new Point(0, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - // gap positions --------------------- var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -2475,7 +2549,7 @@ namespace Barotrauma int basePrice = (GameMain.DebugDraw ? 0 : MainSub?.CalculateBasePrice()) ?? 1000; - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), NumberType.Int, hidePlusMinusButtons: true) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden) { IntValue = Math.Max(MainSub?.Info?.Price ?? basePrice, basePrice), MinValueInt = basePrice, @@ -3151,6 +3225,7 @@ namespace Barotrauma } UpdateEntityList(); + OpenEntityMenu(selectedCategory); } saveFrame = null; @@ -3211,14 +3286,18 @@ namespace Barotrauma SubmarineInfo.RefreshSavedSubs(); SetMode(Mode.Default); - loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) + loadFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); + + new GUIButton(new RectTransform(Vector2.One, loadFrame.RectTransform, Anchor.Center), style: null) { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) loadFrame = null; return true; }, + OnClicked = (_, _) => + { + loadFrame = null; + return true; + } }; - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, loadFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.75f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.53f, 0.75f), loadFrame.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(350, 500) }); var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.01f }; @@ -3908,16 +3987,15 @@ namespace Barotrauma } else { - List availableLayerOptions = new List + + List availableLayers = new List { new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); }) }; + availableLayers.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); - availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); - - ContextMenuOption[] layerOptions = - { - new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayerOptions.ToArray()), + List availableLayerOptions = new List + { new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayers.ToArray()), new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }), new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => { @@ -3927,19 +4005,11 @@ namespace Barotrauma MapEntity.SelectedList.Add(match); } }), - new ContextMenuOption("editor.layer.openlayermenu", isEnabled: true, onSelected: () => - { - previouslyUsedPanel.Visible = false; - undoBufferPanel.Visible = false; - showEntitiesPanel.Visible = false; - layerPanel.Visible = !layerPanel.Visible; - layerPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(visibilityButton.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); - }) }; + availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); GUIContextMenu.CreateContextMenu( new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), - new ContextMenuOption("editor.layer", isEnabled: hasTargets, layerOptions), new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)), new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)), new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), @@ -3951,6 +4021,10 @@ namespace Barotrauma if (!me.Removed) { me.Remove(); } } }), + new ContextMenuOption(string.Empty, isEnabled: false, onSelected: () => { /*do nothing*/ }), + new ContextMenuOption("editor.layer.movetoactivelayer", isEnabled: !(layerList?.SelectedData as string).IsNullOrEmpty(), onSelected: () => { MoveToLayer(layerList.SelectedData as string, targets); }), + new ContextMenuOption("editor.layer.removefromlayer", isEnabled: targets.Any(t => t.Layer != string.Empty), onSelected: () => { targets.ForEach(t => t.Layer = string.Empty); }), + new ContextMenuOption("editor.layeroptions", isEnabled: hasTargets, availableLayerOptions.ToArray()), new ContextMenuOption(TextManager.GetWithVariable("editortip.shiftforextraoptions", "[button]", PlayerInput.SecondaryMouseLabel) + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null)); } } @@ -3961,6 +4035,10 @@ namespace Barotrauma foreach (MapEntity entity in content) { + if (MapEntity.SelectedList.Contains(entity)) + { + MapEntity.ResetEditingHUD(); + } entity.Layer = layer; } } @@ -4006,7 +4084,7 @@ namespace Barotrauma UpdateLayerPanel(); } - private void ReconstructLayers() + public void ReconstructLayers() { ClearLayers(); foreach (MapEntity entity in MapEntity.MapEntityList) @@ -5058,6 +5136,29 @@ namespace Barotrauma } GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); + + if (command is AddOrDeleteCommand addOrDelete) + { + GameMain.SubEditorScreen.EntityAddedOrDeleted(addOrDelete.Receivers); + } + } + + private string prevSelectedLayer; + private void EntityAddedOrDeleted(IEnumerable entities) + { + if (layerList?.SelectedData is string selectedLayer) + { + //add the created entities to the selected layer + foreach (var entity in entities) + { + if (!entity.Removed) + { + entity.Layer = selectedLayer; + } + } + var layerElement = layerList.Content.FindChild(selectedLayer); + layerElement?.Flash(GUIStyle.Green); + } } private void UpdateLayerPanel() @@ -5067,10 +5168,15 @@ namespace Barotrauma layerList.Content.ClearChildren(); layerList.Deselect(); + layerSpecificButtons.ForEach(btn => btn.Enabled = false); GUILayoutGroup buttonHeaders = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), layerList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft); - new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; - new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") + { + ForceUpperCase = ForceUpperCase.Yes, + ToolTip = TextManager.Get("editor.layer.headerlink.tooltip") + }; new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; foreach (var (layer, (visibility, linkage)) in Layers) @@ -5093,7 +5199,11 @@ namespace Barotrauma UpdateLayerPanel(); return false; } - + //hiding a layer automatically deselects it (can't edit a hidden layer) + if (!box.Selected && layerList.SelectedData as string == layer) + { + layerList.Deselect(); + } Layers[layer] = new LayerData(box.Selected ? LayerVisibility.Visible : LayerVisibility.Invisible, data.Linkage); return true; } @@ -5137,7 +5247,7 @@ namespace Barotrauma var btn = child as GUIButton; string originalBtnText = btn.Text.Value; btn.Text = ToolBox.LimitString(btn.Text, btn.Font, btn.Rect.Width); - if (originalBtnText != btn.Text) + if (originalBtnText != btn.Text && btn.ToolTip.IsNullOrEmpty()) { btn.ToolTip = originalBtnText; } @@ -5337,51 +5447,58 @@ namespace Barotrauma } } - if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default) + if (mode == Mode.Default) { - if (dummyCharacter != null) + if (PlayerInput.KeyHit(InputType.Use)) { - if (dummyCharacter.SelectedItem == null) + if (dummyCharacter != null) { - foreach (var entity in MapEntity.HighlightedEntities) + if (dummyCharacter.SelectedItem == null) { - if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) + foreach (var entity in MapEntity.HighlightedEntities) { - var container = item.GetComponents().ToList(); - if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) + if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { - OpenItem(item); - break; + var container = item.GetComponents().ToList(); + if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) + { + OpenItem(item); + break; + } } } } - } - else - { - CloseItem(); + else + { + CloseItem(); + } } } - } - // Focus to selection - if (PlayerInput.KeyHit(Keys.F) && mode == Mode.Default) - { - // content warning: contains coordinate system workarounds - var selected = MapEntity.SelectedList; - if (selected.Count > 0) + // Focus to selection + if (PlayerInput.KeyHit(Keys.F)) { - var dRect = selected.First().Rect; - var rect = new Rectangle(dRect.Left, dRect.Top, dRect.Width, dRect.Height * -1); - if (selected.Count > 1) + // content warning: contains coordinate system workarounds + var selected = MapEntity.SelectedList; + if (selected.Count > 0) { - // Create one big rect out of our selection - selected.Skip(1).ForEach(me => + var dRect = selected.First().Rect; + var rect = new Rectangle(dRect.Left, dRect.Top, dRect.Width, dRect.Height * -1); + if (selected.Count > 1) { - var wRect = me.Rect; - rect = Rectangle.Union(rect, new Rectangle(wRect.Left, wRect.Top, wRect.Width, wRect.Height * -1)); - }); + // Create one big rect out of our selection + selected.Skip(1).ForEach(me => + { + var wRect = me.Rect; + rect = Rectangle.Union(rect, new Rectangle(wRect.Left, wRect.Top, wRect.Width, wRect.Height * -1)); + }); + } + camTargetFocus = rect.Center.ToVector2(); } - camTargetFocus = rect.Center.ToVector2(); + } + if (PlayerInput.KeyHit(Keys.Tab)) + { + entityFilterBox.Select(); } } @@ -5395,11 +5512,6 @@ namespace Barotrauma toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); } - if (PlayerInput.KeyHit(Keys.Tab)) - { - entityFilterBox.Select(); - } - if (PlayerInput.IsCtrlDown() && MapEntity.StartMovingPos == Vector2.Zero) { cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null); @@ -5481,8 +5593,9 @@ namespace Barotrauma foreach (LightComponent lightComponent in item.GetComponents()) { lightComponent.Light.Color = - item.body == null || item.body.Enabled || - (item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems) ? + (item.body == null || item.body.Enabled || item.ParentInventory is ItemInventory { Container.HideItems: true }) && + /*the light is only visible when worn -> can't be visible in the editor*/ + lightComponent.Parent is not Wearable ? lightComponent.LightColor : Color.Transparent; lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 4d9def9a6..69298eef4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -2,10 +2,12 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { @@ -24,6 +26,31 @@ namespace Barotrauma public static DateTime NextCommandPush; public static Tuple CommandBuffer; + private bool isReadonly; + public bool Readonly + { + get => isReadonly; + set + { + foreach (var component in Fields.SelectMany(f => f.Value)) + { + switch (component) + { + case GUINumberInput numInput: + numInput.Readonly = value; + break; + case GUITextBox textBox: + textBox.Readonly = value; + break; + default: + component.Enabled = !value; + break; + } + } + isReadonly = value; + } + } + private Action refresh; public int ContentHeight @@ -478,6 +505,7 @@ namespace Barotrauma GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) { Font = GUIStyle.SmallFont, + Enabled = !Readonly, Selected = value, ToolTip = toolTip, OnSelected = (tickBox) => @@ -528,7 +556,8 @@ namespace Barotrauma var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), NumberType.Int) { ToolTip = toolTip, - Font = GUIStyle.SmallFont + Font = GUIStyle.SmallFont, + Readonly = Readonly }; numberInput.MinValueInt = editableAttribute.MinValueInt; numberInput.MaxValueInt = editableAttribute.MaxValueInt; @@ -572,7 +601,8 @@ namespace Barotrauma numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; numberInput.ValueStep = editableAttribute.ValueStep; - numberInput.ForceShowPlusMinusButtons = editableAttribute.ForceShowPlusMinusButtons; + numberInput.PlusMinusButtonVisibility = editableAttribute + .ForceShowPlusMinusButtons ? GUINumberInput.ButtonVisibility.ForceVisible : default; numberInput.FloatValue = value; numberInput.OnValueChanged += numInput => @@ -690,25 +720,31 @@ namespace Barotrauma public GUIComponent CreateStringField(ISerializableEntity entity, SerializableProperty property, string value, LocalizedString displayName, LocalizedString toolTip) { - var frame = new GUILayoutGroup(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), isHorizontal: true, childAnchor: Anchor.CenterLeft) + bool isItemTagBox = IsItemTagBox(entity, property.Name, out Item it); + var mainFrame = new GUILayoutGroup(new RectTransform(new Point(Rect.Width, isItemTagBox ? elementHeight * 2 : elementHeight), layoutGroup.RectTransform, isFixedSize: true)); + + var frame = new GUILayoutGroup(new RectTransform(isItemTagBox ? new Vector2(1f, 0.5f) : Vector2.One, mainFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont, textAlignment: Alignment.Left) { ToolTip = toolTip }; + Identifier translationTextTag = property.GetAttribute()?.TranslationTextTag ?? Identifier.Empty; - float browseButtonWidth = 0.1f; + const float browseButtonWidth = 0.1f; var editableAttribute = property.GetAttribute(); float textBoxWidth = inputFieldWidth; - if (!translationTextTag.IsEmpty) { textBoxWidth -= browseButtonWidth; } + if (!translationTextTag.IsEmpty || isItemTagBox) { textBoxWidth -= browseButtonWidth; } GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(textBoxWidth, 1), frame.RectTransform)) { Enabled = editableAttribute != null && !editableAttribute.ReadOnly, + Readonly = Readonly, ToolTip = toolTip, Font = GUIStyle.SmallFont, - Text = value, + Text = StripPrefabTags(value), OverflowClip = true }; @@ -725,7 +761,9 @@ namespace Barotrauma propertyBox.OnEnterPressed += (box, text) => OnApply(box); refresh += () => { - if (!propertyBox.Selected) { propertyBox.Text = property.GetValue(entity).ToString(); } + if (propertyBox.Selected) { return; } + + propertyBox.Text = StripPrefabTags(property.GetValue(entity).ToString()); }; bool OnApply(GUITextBox textBox) @@ -743,7 +781,7 @@ namespace Barotrauma if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); - textBox.Text = property.GetValue(entity).ToString(); + textBox.Text = StripPrefabTags(property.GetValue(entity).ToString()); textBox.Flash(GUIStyle.Green, flashDuration: 1f); } //restore the entities that were selected before applying @@ -778,9 +816,67 @@ namespace Barotrauma }; propertyBox.Text = value; } + + if (isItemTagBox) + { + // create prefab tag box + var prefabFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), mainFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), prefabFrame.RectTransform), TextManager.Get("predefinedtags.name"), font: GUIStyle.SmallFont, textAlignment: Alignment.Left) + { + ToolTip = TextManager.Get("predefinedtags.description") + }; + + new GUITextBox(new RectTransform(new Vector2(inputFieldWidth, 1), prefabFrame.RectTransform), createPenIcon: false) + { + Readonly = true, + Font = GUIStyle.SmallFont, + Text = GetPrefabTags(it), + OverflowClip = true, + ToolTip = TextManager.Get("predefinedtags.description") + }; + + // add container tag popup button to the modifiable tag box + new GUIButton(new RectTransform(new Vector2(browseButtonWidth, 1), frame.RectTransform, Anchor.TopRight), "...") + { + OnClicked = (_, _) => { it.CreateContainerTagPicker(propertyBox); return true; } + }; + } + frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { propertyBox }); } return frame; + + static bool IsItemTagBox(ISerializableEntity entity, string propertyName, [NotNullWhen(true)] out Item it) + { + if (entity is Item item && propertyName.Equals(nameof(Item.Tags), StringComparison.OrdinalIgnoreCase)) + { + it = item; + return true; + } + it = null; + return false; + } + + string StripPrefabTags(string text) + { + if (!isItemTagBox) { return text; } + + string prefabTags = GetPrefabTags(it); + if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(prefabTags)) { return text; } + + text = text.Remove(prefabTags); + if (text.StartsWith(",")) + { + text = text.Remove(0, 1); + } + return text; + } + + static string GetPrefabTags(Item it) => string.Join(',', it.Prefab.Tags); } public GUIComponent CreatePointField(ISerializableEntity entity, SerializableProperty property, Point value, LocalizedString displayName, LocalizedString toolTip) @@ -886,7 +982,8 @@ namespace Barotrauma numberInput.MaxValueFloat = editableAttribute.MaxValueFloat; numberInput.DecimalsToDisplay = editableAttribute.DecimalCount; numberInput.ValueStep = editableAttribute.ValueStep; - numberInput.ForceShowPlusMinusButtons = editableAttribute.ForceShowPlusMinusButtons; + numberInput.PlusMinusButtonVisibility = editableAttribute + .ForceShowPlusMinusButtons ? GUINumberInput.ButtonVisibility.ForceVisible : default; numberInput.FloatValue = i == 0 ? value.X : value.Y; @@ -1275,7 +1372,11 @@ namespace Barotrauma // Set the label to be (i + 1) so it's easier to understand for non-programmers string componentLabel = (i + 1).ToString(); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), elementLayoutGroup.RectTransform) { MaxSize = new Point(25, elementLayoutGroup.Rect.Height) }, componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); - GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUIStyle.SmallFont }; + GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) + { + Font = GUIStyle.SmallFont, + Readonly = Readonly + }; int comp = i; textBox.OnEnterPressed += (textBox, text) => OnApply(textBox); textBox.OnDeselected += (textBox, keys) => OnApply(textBox); @@ -1387,7 +1488,7 @@ namespace Barotrauma private bool SetPropertyValue(SerializableProperty property, object entity, object value) { - if (LockEditing || IsEntityRemoved(entity)) { return false; } + if (LockEditing || IsEntityRemoved(entity) || Readonly) { return false; } object oldData = property.GetValue(entity); // some properties have null as the default string value diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index f8c7eacf5..0e5e54d1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -270,6 +270,7 @@ namespace Barotrauma Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v); Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v); + Spacer(right); Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, v => unsavedConfig.Graphics.LosMode = v); @@ -683,22 +684,22 @@ namespace Barotrauma private void CreateGameplayTab() { GUIFrame content = CreateNewContentFrame(Tab.Gameplay); - - var (left, right) = CreateSidebars(content); + + var (leftColumn, rightColumn) = CreateSidebars(content, split: true); var languages = TextManager.AvailableLanguages .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) .ToArray(); - Label(left, TextManager.Get("Language"), GUIStyle.SubHeadingFont); - Dropdown(left, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); - Spacer(left); - - Tickbox(left, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); - Spacer(left); - - Tickbox(left, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); + Label(leftColumn, TextManager.Get("Language"), GUIStyle.SubHeadingFont); + Dropdown(leftColumn, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); + Spacer(leftColumn); + + Tickbox(leftColumn, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); + Spacer(leftColumn); + + Tickbox(leftColumn, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); var resetInGameHintsButton = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), left.RectTransform), + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), leftColumn.RectTransform), TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") { OnClicked = (button, o) => @@ -716,22 +717,41 @@ namespace Barotrauma return false; } }; - Spacer(left); - Label(left, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); - DropdownEnum(left, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); - Spacer(left); + Spacer(leftColumn); - Label(left, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); - Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); - Label(left, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); - Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); - Label(left, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); - Slider(left, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); + Tickbox(leftColumn, TextManager.Get("ChatSpeechBubbles"), TextManager.Get("ChatSpeechBubbles.Tooltip"), unsavedConfig.ChatSpeechBubbles, v => unsavedConfig.ChatSpeechBubbles = v); + Label(leftColumn, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); + DropdownEnum(leftColumn, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); + Spacer(leftColumn); + Label(leftColumn, TextManager.Get("InteractionLabels"), GUIStyle.SubHeadingFont); + DropdownEnum(leftColumn, v => TextManager.Get($"InteractionLabels.{v}"), null, unsavedConfig.InteractionLabelDisplayMode, v => unsavedConfig.InteractionLabelDisplayMode = v); + + Label(rightColumn, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); + Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); + Label(rightColumn, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); + Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); + Label(rightColumn, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); + Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); + Spacer(rightColumn); + var resetSpamListFilter = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform), + TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") + { + OnClicked = static (_, _) => + { + GUI.AskForConfirmation( + header: TextManager.Get("clearserverlistfilters"), + body: TextManager.Get("clearserverlistfiltersconfirmation"), + onConfirm: SpamServerFilters.ClearLocalSpamFilter); + return true; + } + }; + Spacer(rightColumn); #if !OSX - Spacer(right); - var statisticsTickBox = new GUITickBox(NewItemRectT(right), TextManager.Get("statisticsconsenttickbox")) + Spacer(rightColumn); + var statisticsTickBox = new GUITickBox(NewItemRectT(rightColumn), TextManager.Get("statisticsconsenttickbox")) { OnSelected = tickBox => { @@ -780,7 +800,7 @@ namespace Barotrauma if (SteamManager.IsInitialized) { bool shouldCrossplayBeEnabled = unsavedConfig.CrossplayChoice is Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled; - var crossplayTickBox = Tickbox(right, TextManager.Get("EosAllowCrossplay"), TextManager.Get("EosAllowCrossplayTooltip"), shouldCrossplayBeEnabled, v => + var crossplayTickBox = Tickbox(rightColumn, TextManager.Get("EosAllowCrossplay"), TextManager.Get("EosAllowCrossplayTooltip"), shouldCrossplayBeEnabled, v => { unsavedConfig.CrossplayChoice = v ? Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled @@ -792,21 +812,6 @@ namespace Barotrauma crossplayTickBox.ToolTip = TextManager.Get("CantAccessEOSSettingsInMP"); } } - - Spacer(right); - var resetSpamListFilter = - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), right.RectTransform), - TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") - { - OnClicked = static (_, _) => - { - GUI.AskForConfirmation( - header: TextManager.Get("clearserverlistfilters"), - body: TextManager.Get("clearserverlistfiltersconfirmation"), - onConfirm: SpamServerFilters.ClearLocalSpamFilter); - return true; - } - }; } private void CreateModsTab(out WorkshopMenu workshopMenu) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index d050e09c4..f5145ff10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -31,6 +31,7 @@ namespace Barotrauma.Sounds return; } + Loading = true; TaskPool.Add( $"LoadSamples {filename}", LoadSamples(reader), @@ -46,6 +47,7 @@ namespace Barotrauma.Sounds playbackAmplitude = result.PlaybackAmplitude; Owner.KillChannels(this); // prevents INVALID_OPERATION error buffers?.Dispose(); buffers = null; + Loading = false; }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index 8c815b9c0..87c38b8de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -24,6 +24,8 @@ namespace Barotrauma.Sounds public readonly bool StreamsReliably; + public bool Loading { get; protected set; } + private readonly SoundManager.SourcePoolIndex sourcePoolIndex = SoundManager.SourcePoolIndex.Default; public virtual SoundManager.SourcePoolIndex SourcePoolIndex { @@ -84,18 +86,34 @@ namespace Barotrauma.Sounds return Owner.IsPlaying(this); } + public bool LogWarningIfStillLoading() + { + if (Loading) + { + if (Level.Loaded is not { Generating: true }) + { + DebugConsole.AddWarning($"Attempted to play the sound {this} while it was still loading."); + } + return true; + } + return false; + } + public virtual SoundChannel Play(float gain, float range, Vector2 position, bool muffle = false) { + LogWarningIfStillLoading(); return new SoundChannel(this, gain, new Vector3(position.X, position.Y, 0.0f), 1.0f, range * 0.4f, range, "default", muffle); } public virtual SoundChannel Play(float gain, float range, float freqMult, Vector2 position, bool muffle = false) { + LogWarningIfStillLoading(); return new SoundChannel(this, gain, new Vector3(position.X, position.Y, 0.0f), freqMult, range * 0.4f, range, "default", muffle); } public virtual SoundChannel Play(Vector3? position, float gain, float freqMult = 1.0f, bool muffle = false) { + LogWarningIfStillLoading(); return new SoundChannel(this, gain, position, freqMult, BaseNear, BaseFar, "default", muffle); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 08e9811ce..d90705fdb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -262,6 +262,9 @@ namespace Barotrauma.Sounds } } + public const float MinFrequencyMultiplier = 0.25f; + public const float MaxFrequencyMultiplier = 4.0f; + public float frequencyMultiplier; public float FrequencyMultiplier { @@ -271,11 +274,11 @@ namespace Barotrauma.Sounds } set { - if (value < 0.25f || value > 4.0f) + if (value is < MinFrequencyMultiplier or > MaxFrequencyMultiplier) { DebugConsole.ThrowError($"Frequency multiplier out of range: {value}" + Environment.StackTrace.CleanupStackTrace()); } - frequencyMultiplier = Math.Clamp(value, 0.25f, 4.0f); + frequencyMultiplier = Math.Clamp(value, MinFrequencyMultiplier, MaxFrequencyMultiplier); if (ALSourceIndex < 0) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index c9a06e685..1c55f6070 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Sounds listenerPosition = value; Al.Listener3f(Al.Position,value.X,value.Y,value.Z); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener position: " + Al.GetErrorString(alError)); } @@ -68,7 +68,7 @@ namespace Barotrauma.Sounds listenerOrientation[0] = value.X; listenerOrientation[1] = value.Y; listenerOrientation[2] = value.Z; Al.Listenerfv(Al.Orientation, listenerOrientation); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener target vector: " + Al.GetErrorString(alError)); } @@ -83,7 +83,7 @@ namespace Barotrauma.Sounds listenerOrientation[3] = value.X; listenerOrientation[4] = value.Y; listenerOrientation[5] = value.Z; Al.Listenerfv(Al.Orientation, listenerOrientation); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener up vector: " + Al.GetErrorString(alError)); } @@ -101,7 +101,7 @@ namespace Barotrauma.Sounds listenerGain = value; Al.Listenerf(Al.Gain, listenerGain); int alError = Al.GetError(); - if (alError != Al.NoError) + if (alError != Al.NoError && !GameMain.IsExiting) { throw new Exception("Failed to set listener gain: " + Al.GetErrorString(alError)); } @@ -860,14 +860,14 @@ namespace Barotrauma.Sounds ReleaseResources(false); - if (!Alc.MakeContextCurrent(IntPtr.Zero)) + if (!Alc.MakeContextCurrent(IntPtr.Zero) && !GameMain.IsExiting) { throw new Exception("Failed to detach the current ALC context! (error code: " + Alc.GetError(alcDevice).ToString() + ")"); } Alc.DestroyContext(alcContext); - if (!Alc.CloseDevice(alcDevice)) + if (!Alc.CloseDevice(alcDevice) && !GameMain.IsExiting) { throw new Exception("Failed to close ALC device!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 3be4a5571..eaee24b70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -632,7 +632,8 @@ namespace Barotrauma } IEnumerable suitableIntensityMusic = Enumerable.Empty(); - if (targetMusic[mainTrackIndex] is { MuteIntensityTracks: false } mainTrack && Screen.Selected == GameMain.GameScreen) + BackgroundMusic mainTrack = targetMusic[mainTrackIndex]; + if (mainTrack is not { MuteIntensityTracks: true } && Screen.Selected == GameMain.GameScreen) { float intensity = currentIntensity; if (mainTrack?.ForceIntensityTrack != null) @@ -770,11 +771,16 @@ namespace Barotrauma private static IEnumerable GetSuitableMusicClips(Identifier musicType, float currentIntensity) { - return musicClips.Where(music => - music != null && - music.Type == musicType && + return musicClips.Where(music => IsSuitableMusicClip(music, musicType, currentIntensity)); + } + + private static bool IsSuitableMusicClip(BackgroundMusic music, Identifier musicType, float currentIntensity) + { + return + music != null && + music.Type == musicType && currentIntensity >= music.IntensityRange.X && - currentIntensity <= music.IntensityRange.Y); + currentIntensity <= music.IntensityRange.Y; } private static Identifier GetCurrentMusicType() @@ -858,35 +864,42 @@ namespace Barotrauma if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded".ToIdentifier(); } } - - float enemyDistThreshold = 5000.0f; - if (targetSubmarine != null) + float intensity = (GameMain.GameSession?.EventManager?.MusicIntensity ?? 0) * 100.0f; + bool anyMonsterMusicAvailable = + musicClips.Any(m => IsSuitableMusicClip(m, "monster".ToIdentifier(), intensity) || IsSuitableMusicClip(m, "monsterambience".ToIdentifier(), intensity)); + + if (anyMonsterMusicAvailable) { - enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); - } - - foreach (Character character in Character.CharacterList) - { - if (character.IsDead || !character.Enabled) continue; - if (!(character.AIController is EnemyAIController enemyAI) || !enemyAI.Enabled || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) { continue; } - + float enemyDistThreshold = 5000.0f; if (targetSubmarine != null) { - if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) - { - return "monster".ToIdentifier(); - } + enemyDistThreshold = Math.Max(enemyDistThreshold, Math.Max(targetSubmarine.Borders.Width, targetSubmarine.Borders.Height) * 2.0f); } - else if (Character.Controlled != null) + foreach (Character character in Character.CharacterList) { - if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) + if (character.IsDead || !character.Enabled) { continue; } + if (character.AIController is not EnemyAIController { Enabled: true } enemyAI) { continue; } + if (!enemyAI.AttackHumans && !enemyAI.AttackRooms) { continue; } + + if (targetSubmarine != null) { - return "monster".ToIdentifier(); + if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) + { + return "monster".ToIdentifier(); + } + } + else if (Character.Controlled != null) + { + if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) + { + return "monster".ToIdentifier(); + } } } } + if (GameMain.GameSession != null) { if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 8e84334b9..8bc574c86 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -34,6 +34,8 @@ namespace Barotrauma.Sounds public bool UseRadioFilter; public bool UseMuffleFilter; + public bool UsingRadio; + public float Near { get; private set; } public float Far { get; private set; } @@ -55,7 +57,7 @@ namespace Barotrauma.Sounds { if (soundChannel == null) { return; } gain = value; - soundChannel.Gain = value * GameSettings.CurrentConfig.Audio.VoiceChatVolume; + soundChannel.Gain = value * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume; } } @@ -64,8 +66,11 @@ namespace Barotrauma.Sounds get { return soundChannel?.CurrentAmplitude ?? 0.0f; } } - public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, $"VoIP ({name})", true, true, getFullPath: false) + private Client client; + + public VoipSound(Client targetClient, SoundManager owner, VoipQueue q) : base(owner, $"VoIP ({targetClient.Name})", true, true, getFullPath: false) { + client = targetClient; decoder = VoipConfig.CreateDecoder(); ALFormat = Al.FormatMono16; @@ -99,7 +104,7 @@ namespace Barotrauma.Sounds public void ApplyFilters(short[] buffer, int readSamples) { - float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume; + float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume * client.VoiceVolume; for (int i = 0; i < readSamples; i++) { float fVal = ShortToFloat(buffer[i]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index c1c8efe3f..bbd74e417 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -30,6 +30,11 @@ namespace Barotrauma Noise } + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] + public float BlinkFrequency { get; private set; } + + private float blinkTimer = 0.0f; + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 Offset { get; private set; } @@ -242,7 +247,16 @@ namespace Barotrauma } } if (!spriteState.IsActive) { continue; } - + if (decorativeSprite.BlinkFrequency > 0.0f) + { + decorativeSprite.blinkTimer += deltaTime * decorativeSprite.BlinkFrequency; + decorativeSprite.blinkTimer %= 1.0f; + if (decorativeSprite.blinkTimer > 0.5f) + { + spriteState.IsActive = false; + continue; + } + } //check if the sprite should be animated bool animate = true; foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 94f4d5547..c253984c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -240,7 +240,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError($"Sprite \"{file}\" not found!"); + DebugConsole.ThrowError($"Sprite \"{file}\" not found!", contentPackage: contentPackage); DebugConsole.Log(Environment.StackTrace.CleanupStackTrace()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 3eb48d01a..e74ee34bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -29,6 +29,8 @@ namespace Barotrauma /// private bool forcePlaySounds; + private CoroutineHandle playSoundAfterLoadedCoroutine; + partial void InitProjSpecific(ContentXElement element, string parentDebugName) { particleEmitters = new List(); @@ -150,9 +152,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); - ignoreMuffling = sound.IgnoreMuffling; - if (soundChannel != null) { soundChannel.Looping = loopSound; } + PlaySoundOrDelayIfNotLoaded(sound); } } else @@ -177,9 +177,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier()); - ignoreMuffling = selectedSound.IgnoreMuffling; - if (soundChannel != null) { soundChannel.Looping = loopSound; } + PlaySoundOrDelayIfNotLoaded(selectedSound); } } else @@ -193,6 +191,46 @@ namespace Barotrauma soundEmitter = entity; loopStartTime = Timing.TotalTime; } + + void PlaySoundOrDelayIfNotLoaded(RoundSound selectedSound) + { + if (playSoundAfterLoadedCoroutine != null) { return; } + if (selectedSound.Sound.Loading) + { + playSoundAfterLoadedCoroutine = CoroutineManager.StartCoroutine(PlaySoundAfterLoaded(selectedSound)); + } + else + { + PlaySound(selectedSound); + } + } + + IEnumerable PlaySoundAfterLoaded(RoundSound selectedSound) + { + float maxWaitTimer = 1.0f; + while (selectedSound.Sound.Loading && maxWaitTimer > 0.0f) + { + maxWaitTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + if (!selectedSound.Sound.Loading) + { + PlaySound(selectedSound); + } + yield return CoroutineStatus.Success; + } + + void PlaySound(RoundSound selectedSound) + { + //if the sound loops, we must make sure the existing channel + System.Diagnostics.Debug.Assert( + soundChannel == null || !soundChannel.IsPlaying || soundChannel.FadingOutAndDisposing || !soundChannel.Looping, + "A StatusEffect attempted to play a sound, but an looping sound is already playing. The looping sound should be stopped before playing a new one, or it will keep looping indefinitely."); + + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier()); + ignoreMuffling = selectedSound.IgnoreMuffling; + if (soundChannel != null) { soundChannel.Looping = loopSound; } + } } static partial void UpdateAllProjSpecific(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 8c9a12c07..ca2bd8eb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -143,7 +143,10 @@ namespace Barotrauma.Steam { OnClicked = (b, _) => { - SelectTab(tab); + if (tab != CurrentTab) + { + SelectTab(tab); + } return false; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index fe3bc6a7b..b2f6731d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -386,7 +386,7 @@ namespace Barotrauma.Steam private IEnumerable MessageBoxCoroutine(Func> subcoroutine) { - var messageBox = new GUIMessageBox("", TextManager.Get("ellipsis"), buttons: new [] { TextManager.Get("Cancel") }); + var messageBox = new GUIMessageBox("", TextManager.Get("ellipsis").Fallback("..."), buttons: new [] { TextManager.Get("Cancel") }); messageBox.Buttons[0].OnClicked = (button, o) => { messageBox.Close(); @@ -494,7 +494,8 @@ namespace Barotrauma.Steam stagingReady = true; stagingException = t.Exception?.GetInnermost(); }); - currentStepText.Text = TextManager.Get("PublishPopupStaging"); + TrySetText("PublishPopupStaging"); + while (!stagingReady) { yield return new WaitForSeconds(0.5f); } if (stagingException != null) @@ -519,7 +520,7 @@ namespace Barotrauma.Steam } resultException = t.Exception?.GetInnermost(); }); - currentStepText.Text = TextManager.Get("PublishPopupSubmit"); + TrySetText("PublishPopupSubmit"); while (!result.HasValue && resultException is null) { yield return new WaitForSeconds(0.5f); } if (result is { Success: true }) @@ -567,7 +568,7 @@ namespace Barotrauma.Steam }); while (!installed) { - currentStepText.Text = TextManager.Get("PublishPopupInstall"); + TrySetText("PublishPopupInstall"); yield return new WaitForSeconds(0.5f); } @@ -601,6 +602,14 @@ namespace Barotrauma.Steam SteamManager.Workshop.DeletePublishStagingCopy(); messageBox.Close(); + + void TrySetText(string textTag) + { + if (currentStepText?.Text != null) + { + currentStepText.Text = TextManager.Get(textTag); + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index 50a3f5f90..b47221c6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -112,7 +112,7 @@ namespace Barotrauma internal class AddOrDeleteCommand : Command { private readonly Dictionary PreviousInventories = new Dictionary(); - private readonly List Receivers; + public readonly List Receivers; private readonly List CloneList; private readonly bool WasDeleted; private readonly List ContainedItemsCommand = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index 047dce5cd..c17369cd4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; +using System.Text; using Barotrauma.Networking; using Color = Microsoft.Xna.Framework.Color; @@ -422,6 +423,38 @@ namespace Barotrauma return str; } + /// + /// Removes lines on a multi-line string until it fits within the specified height and adds "..." to the end if the string is too long. + /// Doesn't really do anything if the string is only one line, should mostly be used with . + /// + public static string LimitStringHeight(string str, ScalableFont font, int maxHeight) + { + if (maxHeight <= 0 || string.IsNullOrWhiteSpace(str)) { return string.Empty; } + + float currHeight = font.MeasureString("...").Y; + var lines = str.Split('\n'); + + var sb = new StringBuilder(); + foreach (string line in lines) + { + var (lineX, lineY) = font.MeasureString(line); + currHeight += lineY; + if (currHeight > maxHeight) + { + var modifiedLine = line; + while (font.MeasureString($"{modifiedLine}...").X > lineX) + { + modifiedLine = modifiedLine[..^1]; + } + sb.AppendLine($"{modifiedLine}..."); + return sb.ToString(); + } + sb.AppendLine(line); + } + + return str; + } + public static Color GradientLerp(float t, params Color[] gradient) { if (!MathUtils.IsValid(t)) { return Color.Purple; } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index bc6057d8b..1fd816abb 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6fbbca0c5..b3e7a9210 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index c0ab50d09..fa8b4f45c 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 65273750f..b8eabe66b 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index f162a12e0..ec6c9f0cf 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 4002d6ebe..50fa97fdb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -16,7 +16,7 @@ namespace Barotrauma public void ApplyDeathEffects() { - RespawnManager.ReduceCharacterSkills(this); + RespawnManager.ReduceCharacterSkillsOnDeath(this); RemoveSavedStatValuesOnDeath(); CauseOfDeath = null; } @@ -68,8 +68,7 @@ namespace Barotrauma msg.WriteColorR8G8B8(Head.SkinColor); msg.WriteColorR8G8B8(Head.HairColor); msg.WriteColorR8G8B8(Head.FacialHairColor); - - msg.WriteString(ragdollFileName); + msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); msg.WriteIdentifier(MinReputationToHire.factionId); if (!MinReputationToHire.factionId.IsEmpty) @@ -80,11 +79,13 @@ namespace Barotrauma { msg.WriteUInt32(Job.Prefab.UintIdentifier); msg.WriteByte((byte)Job.Variant); - var skills = Job.Prefab.Skills.OrderBy(s => s.Identifier); + + var skills = Job.GetSkills().OrderBy(s => s.Identifier); msg.WriteByte((byte)skills.Count()); - foreach (SkillPrefab skillPrefab in skills) + foreach (var skill in skills) { - msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f); + msg.WriteIdentifier(skill.Identifier); + msg.WriteSingle(skill.Level); } } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 5b637a7d9..45c92fa99 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -75,7 +75,7 @@ namespace Barotrauma NetConfig.HighPrioCharacterPositionUpdateInterval, priority); - if (IsDead) + if (IsDead && !AnimController.IsDraggedWithRope) { interval = Math.Max(interval * 2, 0.1f); } @@ -594,6 +594,31 @@ namespace Barotrauma } } break; + case LatchedOntoTargetEventData latchedOntoTargetEventData: + msg.WriteBoolean(latchedOntoTargetEventData.IsLatched); + if (latchedOntoTargetEventData.IsLatched) + { + msg.WriteSingle(SimPosition.X); + msg.WriteSingle(SimPosition.Y); + msg.WriteSingle(latchedOntoTargetEventData.AttachSurfaceNormal.X); + msg.WriteSingle(latchedOntoTargetEventData.AttachSurfaceNormal.Y); + msg.WriteSingle(latchedOntoTargetEventData.AttachPos.X); + msg.WriteSingle(latchedOntoTargetEventData.AttachPos.Y); + msg.WriteInt32(latchedOntoTargetEventData.TargetLevelWallIndex); + if (latchedOntoTargetEventData.TargetStructureID != NullEntityID) + { + msg.WriteUInt16(latchedOntoTargetEventData.TargetStructureID); + } + else if (latchedOntoTargetEventData.TargetCharacterID != NullEntityID) + { + msg.WriteUInt16(latchedOntoTargetEventData.TargetCharacterID); + } + else + { + msg.WriteUInt16(NullEntityID); + } + } + break; default: throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index c9963d878..5b4a44518 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1186,7 +1186,9 @@ namespace Barotrauma if (GameMain.Server == null) { return; } GameMain.Server.ServerSettings.SetPassword(args.Length > 0 ? args[0] : ""); NewMessage(client.Name + " " + (GameMain.Server.ServerSettings.HasPassword ? " changed the server password to \"" + args[0] + "\"." : " removed password protection from the server.")); - GameMain.Server.SendConsoleMessage(GameMain.Server.ServerSettings.HasPassword ? "Changed the server password." : "Removed password protection from the server.", client); + GameMain.Server.SendChatMessage( + TextManager.GetWithVariable(GameMain.Server.ServerSettings.HasPassword ? "PasswordChangedByClient" : "PasswordRemovedByClient", "[clientname]", client.Name).Value, + ChatMessageType.Server); }); commands.Add(new Command("setmaxplayers|maxplayers", "setmaxplayers [max players]: Sets the maximum player count of the server that's being hosted.", (string[] args) => @@ -1279,12 +1281,11 @@ namespace Barotrauma commands.Add(new Command("servername", "servername [name]: Change the name of the server.", (string[] args) => { GameMain.Server.ServerName = string.Join(" ", args); - GameMain.NetLobbyScreen.ChangeServerName(string.Join(" ", args)); })); commands.Add(new Command("servermsg", "servermsg [message]: Change the message displayed in the server lobby.", (string[] args) => { - GameMain.NetLobbyScreen.ChangeServerMessage(string.Join(" ", args)); + GameMain.Server.ServerSettings.ServerMessageText = string.Join(" ", args); })); commands.Add(new Command("seed|levelseed", "seed/levelseed: Changes the level seed for the next round.", (string[] args) => diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 9653e372e..cf7fdf067 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -48,6 +48,7 @@ namespace Barotrauma spawnInfo[target].OriginalInventoryID, spawnInfo[target].OriginalItemContainerIndex, spawnInfo[target].OriginalSlotIndex); + msg.WriteUInt16(target.ParentTarget?.Item?.ID ?? Entity.NullEntityID); } msg.WriteByte((byte)spawnInfo[target].ExecutedEffectIndices.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0081b16f2..abeeeb1df 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -470,6 +470,11 @@ namespace Barotrauma return characterData.Find(cd => cd.MatchesClient(client)); } + public CharacterCampaignData GetCharacterData(CharacterInfo characterInfo) + { + return characterData.Find(cd => cd.CharacterInfo == characterInfo); + } + public CharacterCampaignData SetClientCharacterData(Client client) { characterData.RemoveAll(cd => cd.MatchesClient(client)); @@ -970,7 +975,7 @@ namespace Barotrauma { int desiredQuantity = purchasedItem.Quantity; if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) && - alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab) is { } alreadyPurchased) + alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab && p.DeliverImmediately == purchasedItem.DeliverImmediately) is { } alreadyPurchased) { desiredQuantity -= alreadyPurchased.Quantity; } @@ -1198,14 +1203,13 @@ namespace Barotrauma if (fireCharacter) { firedIdentifier = msg.ReadInt32(); } Location location = map?.CurrentLocation; - List hiredCharacters = new List(); CharacterInfo firedCharacter = null; if (location != null && AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) { if (fireCharacter) { - firedCharacter = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); + firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier); if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true)) { CrewManager.FireCharacter(firedCharacter); @@ -1221,7 +1225,7 @@ namespace Barotrauma CharacterInfo characterInfo = null; if (existingCrewMember && CrewManager != null) { - characterInfo = CrewManager.CharacterInfos.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); + characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier); } else if(!existingCrewMember && location.HireManager != null) { @@ -1251,10 +1255,7 @@ namespace Barotrauma { foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) { - if (TryHireCharacter(location, hireInfo, sender.Character, sender)) - { - hiredCharacters.Add(hireInfo); - } + TryHireCharacter(location, hireInfo, client: sender); } } @@ -1271,7 +1272,7 @@ namespace Barotrauma } pendingHireInfos.Add(match); - if (pendingHireInfos.Count + CrewManager.CharacterInfos.Count() >= CrewManager.MaxCrewSize) + if (pendingHireInfos.Count + CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { break; } @@ -1281,7 +1282,7 @@ namespace Barotrauma location.HireManager.AvailableCharacters.ForEachMod(info => { - if(!location.HireManager.PendingHires.Contains(info)) + if (!location.HireManager.PendingHires.Contains(info)) { location.HireManager.RenameCharacter(info, info.OriginalName); } @@ -1292,11 +1293,11 @@ namespace Barotrauma // bounce back if (renameCharacter && existingCrewMember) { - SendCrewState(hiredCharacters, (renamedIdentifier, newName), firedCharacter); + SendCrewState((renamedIdentifier, newName), firedCharacter); } else { - SendCrewState(hiredCharacters, default, firedCharacter); + SendCrewState(firedCharacter: firedCharacter); } } @@ -1310,7 +1311,7 @@ namespace Barotrauma /// the client and the server when there's only one person on the server but when a second person joins both of /// their available hires are different from the server. /// - public void SendCrewState(List hiredCharacters, (int id, string newName) renamedCrewMember, CharacterInfo firedCharacter) + public void SendCrewState((int id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null) { List availableHires = new List(); List pendingHires = new List(); @@ -1332,21 +1333,19 @@ namespace Barotrauma hire.ServerWrite(msg); msg.WriteInt32(hire.Salary); } - + msg.WriteUInt16((ushort)pendingHires.Count); foreach (CharacterInfo pendingHire in pendingHires) { msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName()); } - msg.WriteUInt16((ushort)(hiredCharacters?.Count ?? 0)); - if(hiredCharacters != null) + var hiredCharacters = CrewManager.GetCharacterInfos().Where(ci => ci.IsNewHire); + msg.WriteUInt16((ushort)hiredCharacters.Count()); + foreach (CharacterInfo info in hiredCharacters) { - foreach (CharacterInfo info in hiredCharacters) - { - info.ServerWrite(msg); - msg.WriteInt32(info.Salary); - } + info.ServerWrite(msg); + msg.WriteInt32(info.Salary); } bool validRenaming = renamedCrewMember.id > -1 && !string.IsNullOrEmpty(renamedCrewMember.newName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index 6f980eab7..cc154a863 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -27,6 +27,9 @@ namespace Barotrauma.Items.Components //opening a partially stuck door makes it less stuck if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } + ActionType actionType = open ? ActionType.OnOpen : ActionType.OnClose; + item.ApplyStatusEffects(actionType, deltaTime: 1.0f); + if (sendNetworkMessage) { item.CreateServerEvent(this, new EventData(forcedOpen)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs index 11342ebed..a322888cb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -11,20 +11,20 @@ namespace Barotrauma.Items.Components if (!Snapped) { msg.WriteUInt16(target?.ID ?? Entity.NullEntityID); - if (source is Entity entity && !entity.Removed) + switch (source) { - msg.WriteUInt16(entity?.ID ?? Entity.NullEntityID); - msg.WriteByte((byte)0); - } - else if (source is Limb limb && limb.character != null && !limb.character.Removed) - { - msg.WriteUInt16(limb.character?.ID ?? Entity.NullEntityID); - msg.WriteByte((byte)limb.character.AnimController.Limbs.IndexOf(limb)); - } - else - { - msg.WriteUInt16(Entity.NullEntityID); - msg.WriteByte((byte)0); + case Entity { Removed: false } entity: + msg.WriteUInt16(entity.ID); + msg.WriteByte((byte)0); + break; + case Limb { character.Removed: false } limb: + msg.WriteUInt16(limb.character.ID); + msg.WriteByte((byte)limb.character.AnimController.Limbs.IndexOf(limb)); + break; + default: + msg.WriteUInt16(Entity.NullEntityID); + msg.WriteByte((byte)0); + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs index b0b24e75c..25d808849 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CircuitBox.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Items.Components return (msg, deliveryMethod); } - public void CreateServerEvent(INetSerializableStruct data) + public void CreateServerEvent(INetSerializableStruct data) => item.CreateServerEvent(this, new CircuitBoxEventData(data)); public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null) @@ -120,7 +120,7 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.AddComponent: { var data = INetSerializableStruct.Read(msg); - if (!item.CanClientAccess(c)) { break; } + if (!CanAccessAndUnlocked(c)) { break; } var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.PrefabIdentifier); if (prefab is null) @@ -158,14 +158,14 @@ namespace Barotrauma.Items.Components var data = INetSerializableStruct.Read(msg); if (!item.CanClientAccess(c)) { break; } - MoveNodesInternal(data.TargetIDs, data.IOs, data.MoveAmount); + MoveNodesInternal(data.TargetIDs, data.IOs, data.LabelIDs, data.MoveAmount); CreateServerEvent(data); break; } case CircuitBoxOpcode.DeleteComponent: { var data = INetSerializableStruct.Read(msg); - if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + if (!data.TargetIDs.Any() || !CanAccessAndUnlocked(c)) { break; } CreateRefundItemsForUsedResources(data.TargetIDs, c.Character); GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogComponentName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); @@ -180,6 +180,7 @@ namespace Barotrauma.Items.Components SelectComponentsInternal(data.TargetIDs, c.CharacterID, data.Overwrite); SelectInputOutputInternal(data.IOs, c.CharacterID, data.Overwrite); + SelectLabelsInternal(data.LabelIDs, c.CharacterID, data.Overwrite); BroadcastSelectionStatus(); break; } @@ -195,7 +196,7 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.AddWire: { var data = INetSerializableStruct.Read(msg); - if (!item.CanClientAccess(c)) { break; } + if (!CanAccessAndUnlocked(c)) { break; } var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.SelectedWirePrefabIdentifier); if (prefab is null) @@ -229,13 +230,56 @@ namespace Barotrauma.Items.Components case CircuitBoxOpcode.RemoveWire: { var data = INetSerializableStruct.Read(msg); - if (!data.TargetIDs.Any() || !item.CanClientAccess(c)) { break; } + if (!data.TargetIDs.Any() || !CanAccessAndUnlocked(c)) { break; } GameServer.Log($"{NetworkMember.ClientLogName(c)} removed {GetLogWireName(data.TargetIDs)} from circuit box.", ServerLog.MessageType.Wiring); RemoveWireInternal(data.TargetIDs); CreateServerEvent(data); break; } + case CircuitBoxOpcode.RenameLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.AddLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + ushort id = ICircuitBoxIdentifiable.FindFreeID(Labels); + if (id is ICircuitBoxIdentifiable.NullComponentID) + { + ThrowError("Unable to add label because there are no available IDs left.", c); + return; + } + + AddLabelInternal(id, data.Color, data.Position, data.Header, data.Body); + CreateServerEvent(new CircuitBoxServerAddLabelEvent(id, data.Position, new Vector2(256), data.Color, data.Header, data.Body)); + break; + } + case CircuitBoxOpcode.RemoveLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + RemoveLabelInternal(data.TargetIDs); + CreateServerEvent(data); + break; + } + case CircuitBoxOpcode.ResizeLabel: + { + var data = INetSerializableStruct.Read(msg); + if (!CanAccessAndUnlocked(c)) { break; } + + ResizeLabelInternal(data.ID, data.Position, data.Size); + CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) }); + break; + } default: throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events"); } @@ -253,6 +297,8 @@ namespace Barotrauma.Items.Components return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire"; } + + bool CanAccessAndUnlocked(Client client) => item.CanClientAccess(client) && !Locked; } /// @@ -280,6 +326,7 @@ namespace Barotrauma.Items.Components CircuitBoxInitializeStateFromServerEvent data = new( Components: Components.Select(EventFromComponent).ToImmutableArray(), Wires: Wires.Select(EventFromWire).ToImmutableArray(), + Labels: Labels.Select(EventFromLabel).ToImmutableArray(), InputPos: inputPos, OutputPos: outputPos); @@ -297,6 +344,9 @@ namespace Barotrauma.Items.Components var request = new CircuitBoxClientAddWireEvent(wire.Color, from, to, wire.UsedItemPrefab.UintIdentifier); return new CircuitBoxServerCreateWireEvent(request, wire.ID, backingWire); } + + static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label) + => new(label.ID, label.Position, label.Size, label.Color, label.HeaderText, label.BodyText); } // we don't care about updating the view on server @@ -314,8 +364,9 @@ namespace Barotrauma.Items.Components var nodes = Components.Select(static c => new CircuitBoxIdSelectionPair(c.ID, c.IsSelected ? Option.Some(c.SelectedBy) : Option.None)).ToImmutableArray(); var wires = Wires.Select(static w => new CircuitBoxIdSelectionPair(w.ID, w.IsSelected ? Option.Some(w.SelectedBy) : Option.None)).ToImmutableArray(); var ios = InputOutputNodes.Select(static n => new CircuitBoxTypeSelectionPair(n.NodeType, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); + var labels = Labels.Select(static n => new CircuitBoxIdSelectionPair(n.ID, n.IsSelected ? Option.Some(n.SelectedBy) : Option.None)).ToImmutableArray(); - CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios)); + CreateServerEvent(new CircuitBoxServerUpdateSelection(nodes, wires, ios, labels)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 0ce5b7ab5..3d42626e3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -13,8 +13,6 @@ namespace Barotrauma public void ServerEventRead(IReadMessage msg, Client c) { - List prevItems = new List(AllItems.Distinct()); - if (!receivedItemIds.TryGetValue(c, out List[] receivedItemIdsFromClient)) { receivedItemIdsFromClient = new List[capacity]; @@ -60,8 +58,21 @@ namespace Barotrauma return; } - List prevItemInventories = new List() { this }; + //we need to check which of the items the client can access at this point, before we start shuffling things around + //otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding, + //you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet. + Dictionary canAccessItem = new Dictionary(); + for (int i = 0; i < capacity; i++) + { + foreach (ushort id in receivedItemIdsFromClient[i]) + { + if (Entity.FindEntityByID(id) is not Item item) { continue; } + canAccessItem[item] = item.CanClientAccess(c); + } + } + List prevItems = new List(AllItems.Distinct()); + List prevItemInventories = new List() { this }; for (int i = 0; i < capacity; i++) { foreach (Item item in slots[i].Items.ToList()) @@ -119,7 +130,7 @@ namespace Barotrauma var holdable = item.GetComponent(); if (holdable != null && !holdable.CanBeDeattached()) { continue; } - if (!prevItems.Contains(item) && !item.CanClientAccess(c) && + if (!prevItems.Contains(item) && !canAccessItem[item] && (c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory))) { #if DEBUG || UNSTABLE diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs index 2f2a1e174..c5d89f288 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs @@ -1,10 +1,22 @@ -using System; -using Barotrauma.Networking; +using Barotrauma.Networking; +using System; namespace Barotrauma { partial class Submarine { + public readonly struct SetLayerEnabledEventData : NetEntityEvent.IData + { + public readonly Identifier Layer; + public readonly bool Enabled; + + public SetLayerEnabledEventData(Identifier layer, bool enabled) + { + Layer = layer; + Enabled = enabled; + } + } + public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c) { subBody.Body.ServerWrite(tempBuffer); @@ -12,7 +24,15 @@ namespace Barotrauma public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - throw new Exception($"Error while writing a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to send events!"); + if (extraData is SetLayerEnabledEventData setLayerEnabledEventData) + { + msg.WriteIdentifier(setLayerEnabledEventData.Layer); + msg.WriteBoolean(setLayerEnabledEventData.Enabled); + } + else + { + throw new Exception($"Error while writing a network event for the submarine \"{Info.Name} ({ID})\". Unrecognized event data: {extraData?.GetType().Name ?? "null"}"); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 5ffb59bc7..73e844d25 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -349,7 +349,7 @@ namespace Barotrauma.Networking { UInt32 id = incMsg.ReadUInt32(); BannedPlayer? bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); - if (bannedPlayer != null) + if (bannedPlayer != null && c.HasPermission(ClientPermissions.Unban)) { GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.AddressOrAccountId + ")", ServerLog.MessageType.ConsoleUsage); RemoveBan(bannedPlayer); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 1add530fc..6843fc353 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -288,15 +288,19 @@ namespace Barotrauma.Networking GameMain.LuaCs.Hook.Call("client.connected", newClient); - SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); + SendChatMessage($"ServerMessage.JoinedServer~[client]={ClientLogName(newClient)}", ChatMessageType.Server, changeType: PlayerConnectionChangeType.Joined); ServerSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) { string prevNameSanitized = previousPlayer.Name.Replace("‖", ""); - SendChatMessage($"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}", ChatMessageType.Server, null); + SendChatMessage($"ServerMessage.PreviousClientName~[client]={ClientLogName(newClient)}~[previousname]={prevNameSanitized}", ChatMessageType.Server); previousPlayer.Name = newClient.Name; } + if (!ServerSettings.ServerMessageText.IsNullOrEmpty()) + { + SendDirectChatMessage((TextManager.Get("servermotd") + '\n' + ServerSettings.ServerMessageText).Value, newClient, ChatMessageType.Server); + } var savedPermissions = ServerSettings.ClientPermissions.Find(scp => scp.AddressOrAccountId.TryGet(out AccountId accountId) @@ -443,12 +447,12 @@ namespace Barotrauma.Networking endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (subAtLevelEnd && !(GameMain.GameSession?.GameMode is CampaignMode)) + else if (subAtLevelEnd && GameMain.GameSession?.GameMode is not CampaignMode) { endRoundDelay = 5.0f; endRoundTimer += deltaTime; } - else if (isCrewDead && RespawnManager == null) + else if (isCrewDead && (RespawnManager == null || !RespawnManager.CanRespawnAgain)) { #if !DEBUG if (endRoundTimer <= 0.0f) @@ -1148,6 +1152,10 @@ namespace Barotrauma.Networking //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } MissionAction.NotifyMissionsUnlockedThisRound(c); + if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.SendCrewState(); + } c.InGame = true; } } @@ -2343,7 +2351,7 @@ namespace Barotrauma.Networking if (campaign != null) { campaign.CargoManager.CreatePurchasedItems(); - campaign.SendCrewState(null, default, null); + campaign.SendCrewState(); } Level.Loaded?.SpawnNPCs(); @@ -2831,8 +2839,6 @@ namespace Barotrauma.Networking logMsg = message.TextWithSender; } Log(logMsg, ServerLog.MessageType.Chat); - - base.AddChatMessage(message); } private bool ReadClientNameChange(Client c, IReadMessage inc) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index cf6595515..fc721f6f8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -78,6 +78,11 @@ namespace Barotrauma.Networking public bool IsProcessed; + /// + /// Does the client need to be controlling a character for the server to consider the event valid? + /// + public bool RequireCharacter = true; + public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, ReadWriteMessage data) { this.Sender = sender; @@ -157,15 +162,19 @@ namespace Barotrauma.Networking { if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead) { - bufferedEvent.IsProcessed = true; - continue; + if (bufferedEvent.RequireCharacter) + { + bufferedEvent.IsProcessed = true; + continue; + } } //delay reading the events until the inputs for the corresponding frame have been processed //UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored) //atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored - if (!bufferedEvent.Character.IsIncapacitated && + if (bufferedEvent.Character != null && + !bufferedEvent.Character.IsIncapacitated && 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}"); @@ -503,7 +512,12 @@ namespace Barotrauma.Networking byte[] temp = msg.ReadBytes(msgLength - 2); buffer.WriteBytes(temp, 0, msgLength - 2); buffer.BitPosition = 0; - BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer)); + BufferEvent( + new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer) + { + //hull updates can be sent without a character to allow editing water and fire in spectator mode + RequireCharacter = entity is not Hull + }); sender.LastSentEntityEventID++; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 5dafbee3c..aa8879e4b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -277,7 +277,8 @@ namespace Barotrauma.Networking ServerName = GameMain.Server.ServerName, ContentPackages = contentPackages .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) - .ToImmutableArray() + .ToImmutableArray(), + AllowModDownloads = serverSettings.AllowModDownloads }; break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index b0987895d..c284a4e5b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -344,8 +344,8 @@ namespace Barotrauma.Networking var clients = GetClientsToRespawn().ToList(); foreach (Client c in clients) { - //get rid of the existing character - c.Character?.DespawnNow(); + // Get rid of the existing character + if (c.Character is Character character) { character.DespawnNow(); } c.WaitForNextRoundRespawn = null; @@ -390,12 +390,9 @@ namespace Barotrauma.Networking { divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuitdeep")); } - if (divingSuitPrefab == null) - { - divingSuitPrefab = + divingSuitPrefab ??= ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuit")) ?? ItemPrefab.Find(null, "divingsuit".ToIdentifier()); - } ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank".ToIdentifier()); ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter".ToIdentifier()); ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell".ToIdentifier()); @@ -408,6 +405,7 @@ namespace Barotrauma.Networking characterInfos[i].ClearCurrentOrders(); + CharacterCampaignData characterCampaignData = null; bool forceSpawnInMainSub = false; if (!bot) { @@ -419,16 +417,16 @@ namespace Barotrauma.Networking clients[i].PendingName = null; } - var matchingData = campaign?.GetClientCharacterData(clients[i]); - if (matchingData != null) + characterCampaignData = campaign?.GetClientCharacterData(clients[i]); + if (characterCampaignData != null) { - if (!matchingData.HasSpawned) + if (!characterCampaignData.HasSpawned) { forceSpawnInMainSub = true; } else { - ReduceCharacterSkills(characterInfos[i]); + ReduceCharacterSkillsOnDeath(characterInfos[i]); characterInfos[i].RemoveSavedStatValuesOnDeath(); characterInfos[i].CauseOfDeath = null; } @@ -436,6 +434,7 @@ namespace Barotrauma.Networking } var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); + characterCampaignData?.ApplyWalletData(character); character.TeamID = CharacterTeamType.Team1; character.LoadTalents(); @@ -526,11 +525,10 @@ namespace Barotrauma.Networking } var characterData = campaign?.GetClientCharacterData(clients[i]); + // NOTE: This was where Reaper's tax got applied if (characterData != null && Level.Loaded?.Type != LevelData.LevelType.Outpost && characterData.HasSpawned) { - //we need to reapply the previous respawn penalty affliction or successive deaths won't make it stack - characterData.ApplyHealthData(character, (AfflictionPrefab ap) => ap == GetRespawnPenaltyAfflictionPrefab()); - GiveRespawnPenaltyAffliction(character); + ReduceCharacterSkillsOnDeath(characterInfos[i], applyExtraSkillLoss: true); } if (characterData == null || characterData.HasSpawned) { @@ -562,14 +560,37 @@ namespace Barotrauma.Networking } } - public static void ReduceCharacterSkills(CharacterInfo characterInfo) + /// + /// Reduce any skill gains the character may have made over the job's default + /// skill levels by percentages defined in server settings. There are two + /// reductions, a base one that always applies, and an extra loss that only + /// applies when the player chooses to respawn ASAP rather than wait. + /// + public static void ReduceCharacterSkillsOnDeath(CharacterInfo characterInfo, bool applyExtraSkillLoss = false) { if (characterInfo?.Job == null) { return; } + + float resistanceMultiplier; + float skillLossPercentage; + if (applyExtraSkillLoss) + { + DebugConsole.Log($"Calculating extra skill loss on respawn for {characterInfo.Name}:"); + resistanceMultiplier = characterInfo.LastResistanceMultiplierSkillLossRespawn; + skillLossPercentage = SkillLossPercentageOnImmediateRespawn; + } + else + { + DebugConsole.Log($"Calculating base skill loss on death for {characterInfo.Name}:"); + resistanceMultiplier = characterInfo.LastResistanceMultiplierSkillLossDeath; + skillLossPercentage = SkillLossPercentageOnDeath; + } + skillLossPercentage *= resistanceMultiplier; + foreach (Skill skill in characterInfo.Job.GetSkills()) { var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillLossPercentageOnDeath / 100.0f); + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, skillLossPercentage / 100.0f); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index a1d2cf335..92bf12514 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -95,15 +95,6 @@ namespace Barotrauma.Networking { NetFlags requiredFlags = GetRequiredFlags(c); outMsg.WriteByte((byte)requiredFlags); - if (requiredFlags.HasFlag(NetFlags.Name)) - { - outMsg.WriteString(ServerName); - } - - if (requiredFlags.HasFlag(NetFlags.Message)) - { - outMsg.WriteString(ServerMessageText); - } outMsg.WriteByte((byte)PlayStyle); outMsg.WriteByte((byte)MaxPlayers); outMsg.WriteBoolean(HasPassword); @@ -122,8 +113,7 @@ namespace Barotrauma.Networking WriteHiddenSubs(outMsg); } - if (c.HasPermission(Networking.ClientPermissions.ManageSettings) - && NetIdUtils.IdMoreRecent( + if (NetIdUtils.IdMoreRecent( newID: LastUpdateIdForFlag[NetFlags.Properties], oldID: c.LastRecvServerSettingsUpdate)) { @@ -147,20 +137,6 @@ namespace Barotrauma.Networking bool changed = false; - if (flags.HasFlag(NetFlags.Name)) - { - string serverName = incMsg.ReadString(); - if (ServerName != serverName) { changed = true; } - ServerName = serverName; - } - - if (flags.HasFlag(NetFlags.Message)) - { - string serverMessageText = incMsg.ReadString(); - if (ServerMessageText != serverMessageText) { changed = true; } - ServerMessageText = serverMessageText; - } - if (flags.HasFlag(NetFlags.Properties)) { bool propertiesChanged = ReadExtraCargo(incMsg); @@ -217,42 +193,9 @@ namespace Barotrauma.Networking int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); - bool changedTraitorProbability = incMsg.ReadBoolean(); - float traitorProbability = incMsg.ReadSingle(); - if (changedTraitorProbability) - { - TraitorProbability = traitorProbability; - } //the byte indicates the direction we're changing the value, subtract one to get negative values from a byte TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1; - int botCount = BotCount + incMsg.ReadByte() - 1; - if (botCount < 0) { botCount = MaxBotCount; } - if (botCount > MaxBotCount) { botCount = 0; } - BotCount = botCount; - - int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1; - if (botSpawnMode < 0) { botSpawnMode = 1; } - if (botSpawnMode > 1) { botSpawnMode = 0; } - BotSpawnMode = (BotSpawnMode)botSpawnMode; - - float levelDifficulty = incMsg.ReadSingle(); - if (levelDifficulty >= 0.0f) { SelectedLevelDifficulty = levelDifficulty; } - - bool changedUseRespawnShuttle = incMsg.ReadBoolean(); - bool useRespawnShuttle = incMsg.ReadBoolean(); - if (changedUseRespawnShuttle) - { - UseRespawnShuttle = useRespawnShuttle; - } - - bool changedAutoRestart = incMsg.ReadBoolean(); - bool autoRestart = incMsg.ReadBoolean(); - if (changedAutoRestart) - { - AutoRestart = autoRestart; - } - changed |= true; UpdateFlag(NetFlags.Misc); } @@ -292,8 +235,6 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); - doc.Root.SetAttributeValue("LevelDifficulty", ((int)selectedLevelDifficulty).ToString()); - doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); @@ -304,6 +245,8 @@ namespace Barotrauma.Networking SerializableProperty.SerializeProperties(this, doc.Root, true); doc.Root.Add(CampaignSettings.Save()); + doc.Root.SetAttributeValue("DisabledMonsters", string.Join(",", MonsterEnabled.Where(kvp => !kvp.Value).Select(kvp => kvp.Key.Value))); + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, @@ -351,9 +294,6 @@ namespace Barotrauma.Networking AllowSubVoting = SubSelectionMode == SelectionMode.Vote; AllowModeVoting = ModeSelectionMode == SelectionMode.Vote; - selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f); - GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty); - GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); @@ -448,6 +388,14 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotCount(BotCount); MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + var disabledMonsters = doc.Root.GetAttributeIdentifierArray("DisabledMonsters", Array.Empty()); + foreach (var disabledMonster in disabledMonsters) + { + if (MonsterEnabled.ContainsKey(disabledMonster)) + { + MonsterEnabled[disabledMonster] = false; + } + } foreach (XElement element in doc.Root.Elements()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index acc9cfff3..9a593cac1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -121,13 +121,13 @@ namespace Barotrauma.Networking if (recipientSpectating) { if (recipient.SpectatePos == null) { return true; } - distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / ChatMessage.SpeakRange, 0.0f, 1.0f); + distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / ChatMessage.SpeakRangeVOIP, 0.0f, 1.0f); return distanceFactor < 1.0f; } else { //otherwise do a distance check - float garbleAmount = ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRange); + float garbleAmount = ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRangeVOIP); distanceFactor = garbleAmount; return garbleAmount < range; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index c9c5e57e0..3c3aeaa8c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -106,24 +106,6 @@ namespace Barotrauma } } - public void ChangeServerName(string n) - { - GameMain.Server.ServerSettings.ServerName = n; lastUpdateID++; - } - - public void ChangeServerMessage(string m) - { - GameMain.Server.ServerSettings.ServerMessageText = m; lastUpdateID++; - } - - public List JobPreferences - { - get - { - return null; - } - } - public NetLobbyScreen() { LevelSeed = ToolBox.RandomSeed(8); @@ -175,7 +157,7 @@ namespace Barotrauma } set { - if (levelSeed == value) return; + if (levelSeed == value) { return; } lastUpdateID++; levelSeed = value; @@ -205,6 +187,12 @@ namespace Barotrauma { GameMain.GameSession = null; } + if (GameMain.Server.ServerSettings.SelectedSubmarine.IsNullOrEmpty()) + { + //if no sub is selected in the settings, + //select the random sub we selected in the constructor + GameMain.Server.ServerSettings.SelectedSubmarine = SelectedSub?.Name; + } } public void RandomizeSettings() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index 004e422df..c9c691212 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -334,7 +334,7 @@ namespace Barotrauma private void CreateTraitorEvent(EventManager eventManager, TraitorEventPrefab selectedPrefab, Client traitor) { - if (selectedPrefab.TryCreateInstance(out var newEvent)) + if (selectedPrefab.TryCreateInstance(eventManager.RandomSeed, out var newEvent)) { var secondaryTraitors = SelectSecondaryTraitors(newEvent, traitor); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ce5342530..ffe0a0cae 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.3.0.4 + 1.4.4.1 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index a9be7381b..9ab82603d 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -2,37 +2,81 @@ - - + + + + + MaxMissionCount="2" + WorldHostility="low" + CrewVitalityMultiplier="1.1" + NonCrewVitalityMultiplier="1.0" + OxygenMultiplier="1.2" + FuelMultiplier="1.2" + MissionRewardMultiplier="1.0" + ShopPriceMultiplier="0.9" + ShipyardPriceMultiplier="0.9" + RepairFailMultiplier="1.0" + PatdownProbability="low" + ShowHuskWarning="true"/> + WorldHostility="medium" + CrewVitalityMultiplier="1.0" + NonCrewVitalityMultiplier="1.0" + OxygenMultiplier="1.0" + FuelMultiplier="1.0" + MissionRewardMultiplier="1.0" + ShopPriceMultiplier="1.0" + ShipyardPriceMultiplier="1.0" + RepairFailMultiplier="1.0" + PatdownProbability="medium" + ShowHuskWarning="true"/> + MaxMissionCount="2" + WorldHostility="high" + CrewVitalityMultiplier="1.0" + NonCrewVitalityMultiplier="1.0" + OxygenMultiplier="0.7" + FuelMultiplier="0.9" + MissionRewardMultiplier="1.0" + ShopPriceMultiplier="1.5" + ShipyardPriceMultiplier="1.5" + RepairFailMultiplier="2.0" + PatdownProbability="high" + ShowHuskWarning="false"/> + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index eb469f60a..aa47e8d51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -76,9 +76,9 @@ namespace Barotrauma get { return Character.AnimController.Collider.LinearVelocity; } } - public virtual bool CanEnterSubmarine + public virtual CanEnterSubmarine CanEnterSubmarine { - get { return true; } + get { return Character.AnimController.CanEnterSubmarine; } } public virtual bool CanFlip @@ -327,8 +327,17 @@ namespace Barotrauma { if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags)) { - // Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory. - return false; + bool switchingToBetterSuit = + targetTags != null && + targetTags.FirstOrDefault() == Tags.HeavyDivingGear && + AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, Character) && + !AIObjectiveFindDivingGear.IsSuitablePressureProtection(otherItem, Tags.HeavyDivingGear, Character); + // Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items + // of the same type and if can't fit them all in the inventory. + if (!switchingToBetterSuit) + { + return false; + } } //if everything else fails, simply drop the existing item otherItem.Drop(Character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index d4188f275..0c94adf1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -176,12 +176,13 @@ namespace Barotrauma } } - public override bool CanEnterSubmarine + public override CanEnterSubmarine CanEnterSubmarine { get { - //can't enter a submarine when attached to something - return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub); + //can't enter a submarine when attached to one + if (LatchOntoAI is { IsAttachedToSub: true }) { return CanEnterSubmarine.False; } + return Character.AnimController.CanEnterSubmarine; } } @@ -535,12 +536,27 @@ namespace Barotrauma FadeMemories(updateMemoriesInverval); updateMemoriesTimer = updateMemoriesInverval; } - if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null && - SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target))) + if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null) { - // Keep fleeing if being chased - State = AIState.Flee; + Character target = SelectedAiTarget.Entity as Character; + if (target == null && SelectedAiTarget.Entity is Item targetItem) + { + target = GetOwner(targetItem); + } + bool shouldFlee = false; + if (target != null) + { + // Keep fleeing if being chased or if we see a human target (that don't have enemy ai). + shouldFlee = target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target); + } + // If we should not flee, just idle. Don't allow any other AI state when below the health threshold. + State = shouldFlee ? AIState.Flee : AIState.Idle; wallTarget = null; + if (State != AIState.Flee) + { + SelectedAiTarget = null; + _lastAiTarget = null; + } } else { @@ -614,7 +630,7 @@ namespace Barotrauma steeringManager = outsideSteering; } } - + bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater; bool run = false; switch (State) @@ -870,11 +886,11 @@ namespace Barotrauma } // Ensure that the creature keeps inside the level SteerInsideLevel(deltaTime); - float defaultSpeed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); - //calculate a normalized Steering value at this point: we multiply it with the actual, desired speed in ApplyMovementLimits - steeringManager.Update(1.0f); - float speed = useSteeringLengthAsMovementSpeed ? Steering.Length() : defaultSpeed; - Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, speed); + float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); + // Doesn't work if less than 1, when we use steering length as movement speed. + steeringManager.Update(Math.Max(speed, 1.0f)); + float movementSpeed = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed; + Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, movementSpeed); if (Character.CurrentHull != null && Character.AnimController.InWater) { // Limit the swimming speed inside the sub. @@ -1087,6 +1103,22 @@ namespace Barotrauma // How long the monster tries to reach out for the target when it's close to it before ignoring it. private const float reachTimeOut = 10; + private bool IsSameTarget(AITarget target, AITarget otherTarget) + { + if (target?.Entity == otherTarget?.Entity) { return true; } + if (IsItemInCharacterInventory(target, otherTarget) || IsItemInCharacterInventory(otherTarget, target)) { return true; } + return false; + + bool IsItemInCharacterInventory(AITarget potentialItem, AITarget potentialCharacter) + { + if (potentialItem?.Entity is Item item && potentialCharacter?.Entity is Character character) + { + return item.ParentInventory?.Owner == character; + } + return false; + } + } + private void UpdateAttack(float deltaTime) { if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) @@ -1132,7 +1164,7 @@ namespace Barotrauma attackSimPos = Character.GetRelativeSimPosition(SelectedAiTarget.Entity); } - if (Character.AnimController.CanEnterSubmarine) + if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True) { if (TrySteerThroughGaps(deltaTime)) { @@ -1177,13 +1209,21 @@ namespace Barotrauma if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) { var currentAttackLimb = AttackLimb ?? _previousAttackLimb; - if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) + if (currentAttackLimb.attack.CoolDownTimer >= + currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack; + currentAttackLimb.attack.AfterAttackTimer += deltaTime; + AIBehaviorAfterAttack activeBehavior = + currentAttackLimb.attack.AfterAttackSecondaryDelay > 0 && currentAttackLimb.attack.AfterAttackTimer > currentAttackLimb.attack.AfterAttackSecondaryDelay ? + currentAttackLimb.attack.AfterAttackSecondary : + currentAttackLimb.attack.AfterAttack; switch (activeBehavior) { + case AIBehaviorAfterAttack.Eat: + UpdateEating(deltaTime); + return; case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: if (currentAttackLimb.attack.SecondaryCoolDown <= 0) @@ -1205,7 +1245,7 @@ namespace Barotrauma if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. - if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) + if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget)) { canAttack = false; if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack) @@ -1266,27 +1306,24 @@ namespace Barotrauma if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. - if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) + if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget)) { UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } + // If the secondary cooldown is defined and expired, check if we can switch the attack + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); + if (newLimb != null) + { + // Attack with the new limb + AttackLimb = newLimb; + } else { - // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); - if (newLimb != null) - { - // Attack with the new limb - AttackLimb = newLimb; - } - else - { - // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); - return; - } - } + // No new limb was found. + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + return; + } } else { @@ -1308,27 +1345,24 @@ namespace Barotrauma if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. - if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) + if (_previousAiTarget != null && !IsSameTarget(SelectedAiTarget, _previousAiTarget)) { UpdateIdle(deltaTime, followLastTarget: false); return; } + // If the secondary cooldown is defined and expired, check if we can switch the attack + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); + if (newLimb != null) + { + // Attack with the new limb + AttackLimb = newLimb; + } else { - // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); - if (newLimb != null) - { - // Attack with the new limb - AttackLimb = newLimb; - } - else - { - // No new limb was found. - UpdateIdle(deltaTime, followLastTarget: false); - return; - } - } + // No new limb was found. + UpdateIdle(deltaTime, followLastTarget: false); + return; + } } else { @@ -1341,6 +1375,9 @@ namespace Barotrauma case AIBehaviorAfterAttack.FollowThrough: UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; + case AIBehaviorAfterAttack.FollowThroughWithoutObstacleAvoidance: + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true, avoidObstacles: false); + return; case AIBehaviorAfterAttack.FallBack: case AIBehaviorAfterAttack.Reverse: default: @@ -1724,7 +1761,7 @@ namespace Barotrauma circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor); aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); DisableAttacksIfLimbNotRanged(); - if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false }) + if (targetSub is { Borders.Width: < 1000 } && AttackLimb?.attack is { Ranged: false }) { breakCircling = true; CirclePhase = CirclePhase.CloseIn; @@ -1982,7 +2019,7 @@ namespace Barotrauma Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; if (AttackLimb?.attack is Attack { Ranged: true } attack) { - AimRangedAttack(attack, targetEntity); + AimRangedAttack(attack, attackTargetLimb as ISpatialEntity ?? targetEntity); } if (canAttack) { @@ -2005,9 +2042,10 @@ namespace Barotrauma } } - public void AimRangedAttack(Attack attack, Entity targetEntity) + public void AimRangedAttack(Attack attack, ISpatialEntity targetEntity) { - if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; } + if (attack is not { Ranged: true }) { return; } + if (targetEntity is Entity { Removed: true }) { return; } Character.SetInput(InputType.Aim, false, true); if (attack.AimRotationTorque <= 0) { return; } Limb limb = GetLimbToRotate(attack); @@ -2115,7 +2153,10 @@ namespace Barotrauma bool wasLatched = IsLatchedOnSub; Character.AnimController.ReleaseStuckLimbs(); - LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); + if (attackResult.Damage > 0) + { + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); + } if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } if (attackResult.Damage >= AIParams.DamageThreshold) { @@ -2283,7 +2324,7 @@ namespace Barotrauma Limb referenceLimb = GetLimbToRotate(ActiveAttack); if (referenceLimb != null) { - Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition; + Vector2 toTarget = attackWorldPos - referenceLimb.WorldPosition; float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir); float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); @@ -2456,7 +2497,7 @@ namespace Barotrauma } private Vector2? attackVector = null; - private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false) + private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false, bool avoidObstacles = true) { if (attackVector == null) { @@ -2468,7 +2509,7 @@ namespace Barotrauma dir = Vector2.UnitY; } steeringManager.SteeringManual(deltaTime, dir); - if (Character.AnimController.InWater && !Reverse) + if (Character.AnimController.InWater && !Reverse && avoidObstacles) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } @@ -2779,10 +2820,16 @@ namespace Barotrauma // Ignore inner walls when outside (walltargets still work) continue; } - if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s)) + bool attemptToGetInside = + Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True || + //characters that are aggressive boarders can partially enter the sub can attempt to push through holes + (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial && IsAggressiveBoarder); + + if (!attemptToGetInside && IsWallDisabled(s)) { continue; } + // Prefer weaker walls (200 is the default for normal hull walls) valueModifier = 200f / s.MaxHealth; for (int i = 0; i < s.Sections.Length; i++) @@ -2790,7 +2837,7 @@ namespace Barotrauma var section = s.Sections[i]; if (section.gap == null) { continue; } bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; - if (Character.AnimController.CanEnterSubmarine) + if (attemptToGetInside) { if (!isCharacterInside) { @@ -2873,9 +2920,11 @@ namespace Barotrauma { if (!canAttackDoors) { continue; } } - else if (!Character.AnimController.CanEnterSubmarine) + else if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.True) { // Ignore broken and open doors, if cannot enter submarine + // Also ignore them if the monster can only partially enter the sub: + // these monsters tend to be too large to get through doors anyway. continue; } if (IsAggressiveBoarder) @@ -2911,6 +2960,13 @@ namespace Barotrauma if (targetParams.IgnoreInside && Character.CurrentHull != null) { continue; } if (targetParams.IgnoreOutside && Character.CurrentHull == null) { continue; } if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; } + if (targetParams.IgnoreTargetInside && aiTarget.Entity.Submarine != null) { continue; } + if (targetParams.IgnoreTargetOutside && aiTarget.Entity.Submarine == null) { continue; } + if (aiTarget.Entity is ISerializableEntity se) + { + if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { continue; } + } + if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } if (targetParams.IgnoreIfNotInSameSub) { if (aiTarget.Entity.Submarine != Character.Submarine) { continue; } @@ -2981,6 +3037,16 @@ namespace Barotrauma { dist *= 0.9f; } + if (targetParams.PerceptionDistanceMultiplier > 0.0f) + { + dist /= targetParams.PerceptionDistanceMultiplier; + } + + if (targetParams.MaxPerceptionDistance > 0.0f && + dist * dist > targetParams.MaxPerceptionDistance * targetParams.MaxPerceptionDistance) + { + continue; + } if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget)) { @@ -3196,7 +3262,7 @@ namespace Barotrauma { if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub) { - if (!(SelectedAiTarget?.Entity is Structure wall)) + if (SelectedAiTarget?.Entity is not Structure wall) { wall = wallTarget?.Structure; } @@ -3251,9 +3317,10 @@ namespace Barotrauma if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; - Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; + Vector2 refPos = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { + Vector2 rayStart = refPos; Vector2 rayEnd = SelectedAiTarget.SimPosition; if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -3267,6 +3334,7 @@ namespace Barotrauma } if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading)) { + Vector2 rayStart = refPos; Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5); if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -3282,6 +3350,7 @@ namespace Barotrauma } if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering)) { + Vector2 rayStart = refPos; Vector2 rayEnd = rayStart + Steering * 5; if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { @@ -3297,18 +3366,25 @@ namespace Barotrauma } if (wallHits.Any()) { + Vector2 targetdiff = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - (AttackLimb != null ? AttackLimb.WorldPosition : WorldPosition)); + float targetDistance = targetdiff.LengthSquared(); Body closestBody = null; float closestDistance = 0; int sectionIndex = -1; Vector2 sectionPos = Vector2.Zero; foreach ((Body body, int index, Vector2 sectionPosition) in wallHits) { - float distance = Vector2.DistanceSquared(SimPosition, sectionPosition); + Structure structure = body.UserData as Structure; + float distance = Vector2.DistanceSquared( + refPos, + Submarine.GetRelativeSimPosition(ConvertUnits.ToSimUnits(sectionPosition), Character.Submarine, structure.Submarine)); + //if the wall is further than the target (e.g. at the other side of the sub?), we shouldn't be targeting it + if (distance > targetDistance) { continue; } if (closestBody == null || closestDistance == 0 || distance < closestDistance) { closestBody = body; closestDistance = distance; - wall = closestBody.UserData as Structure; + wall = structure; sectionPos = sectionPosition; sectionIndex = index; } @@ -3326,14 +3402,18 @@ namespace Barotrauma sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X; } LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); - if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) + if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True || + !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) { - if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine) + if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.True) { bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent() != null; // Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces. if (!isTargetingDoor) { + //TODO: this might cause problems: many wall pieces (like smaller shuttle pieces + //and small decorative wall structures are currently marked as having no AI target, + //which can mean a monster very frequently ignores targets inside because they're blocked by those structures IgnoreTarget(SelectedAiTarget); ResetAITarget(); } @@ -3353,7 +3433,9 @@ namespace Barotrauma void DoRayCast(Vector2 rayStart, Vector2 rayEnd) { - Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); + Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, + ignoreSensors: CanEnterSubmarine != CanEnterSubmarine.False, + ignoreDisabledWalls: CanEnterSubmarine != CanEnterSubmarine.False); if (hitTarget != null && IsValid(hitTarget, out wall)) { int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); @@ -3371,7 +3453,8 @@ namespace Barotrauma { if (wall.SectionBodyDisabled(i)) { - if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount)) + if (Character.AnimController.CanEnterSubmarine != CanEnterSubmarine.False && + CanPassThroughHole(wall, i, requiredHoleCount)) { sectionIndex = i; break; @@ -3394,14 +3477,14 @@ namespace Barotrauma { wall = null; if (Submarine.LastPickedFraction == 1.0f) { return false; } - if (!(hit.UserData is Structure w)) { return false; } + if (hit.UserData is not Structure w) { return false; } if (w.Submarine == null) { return false; } if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; } if (Character.Submarine == null) { if (w.Prefab.Tags.Contains("inner")) { - if (!Character.AnimController.CanEnterSubmarine) { return false; } + if (Character.AnimController.CanEnterSubmarine == CanEnterSubmarine.False) { return false; } } else if (!AIParams.TargetOuterWalls) { @@ -3731,7 +3814,7 @@ namespace Barotrauma } reachTimer = 0; sinTime = 0; - if (breakCircling && strikeTimer <= 0) + if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn) { CirclePhase = CirclePhase.Start; } @@ -3757,7 +3840,7 @@ namespace Barotrauma blockCheckTimer = 0; reachTimer = 0; sinTime = 0; - if (breakCircling && strikeTimer <= 0) + if (breakCircling && strikeTimer <= 0 && CirclePhase != CirclePhase.CloseIn) { CirclePhase = CirclePhase.Start; } @@ -3765,7 +3848,12 @@ namespace Barotrauma private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); - private float GetPerceivingRange(AITarget target) => Math.Max(target.SightRange * Sight, target.SoundRange * Hearing); + private float GetPerceivingRange(AITarget target) + { + float maxSightOrSoundRange = Math.Max(target.SightRange * Sight, target.SoundRange * Hearing); + if (AIParams.MaxPerceptionDistance >= 0 && maxSightOrSoundRange > AIParams.MaxPerceptionDistance) { return AIParams.MaxPerceptionDistance; } + return maxSightOrSoundRange; + } private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false) { @@ -3783,6 +3871,7 @@ namespace Barotrauma } if (dist > 0) { + if (AIParams.MaxPerceptionDistance >= 0 && dist > AIParams.MaxPerceptionDistance) { return false; } insideSightRange = IsInRange(dist, target.SightRange, Sight); if (!checkVisibility && insideSightRange) { return true; } insideSoundRange = IsInRange(dist, target.SoundRange, Hearing); @@ -3793,6 +3882,7 @@ namespace Barotrauma { distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition); } + if (AIParams.MaxPerceptionDistance >= 0 && distSquared > AIParams.MaxPerceptionDistance * AIParams.MaxPerceptionDistance) { return false; } insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight); if (!checkVisibility && insideSightRange) { return true; } insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index a8dfedc43..706a8fd0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -72,6 +72,11 @@ namespace Barotrauma /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. /// public float ReportRange { get; set; } = float.PositiveInfinity; + + /// + /// How far the character can seek new weapons from. + /// + public float FindWeaponsRange { get; set; } = float.PositiveInfinity; private float _aimSpeed = 1; public float AimSpeed @@ -150,9 +155,13 @@ namespace Barotrauma } public override bool IsMentallyUnstable => - MentalStateManager == null ? false : - MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal && - MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused; + MentalStateManager is + { + CurrentMentalType: + MentalStateManager.MentalType.Afraid or + MentalStateManager.MentalType.Desperate or + MentalStateManager.MentalType.Berserk + }; public ShipCommandManager ShipCommandManager { get; private set; } @@ -817,7 +826,7 @@ namespace Barotrauma private readonly HashSet itemsToRelocate = new HashSet(); - private void HandleRelocation(Item item) + public void HandleRelocation(Item item) { if (item.SpawnedInCurrentOutpost) { return; } if (item.Submarine == null) { return; } @@ -837,7 +846,10 @@ namespace Barotrauma // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. campaign.BeforeLevelLoading += Relocate; } - + campaign.ItemsRelocatedToMainSub = true; +#if CLIENT + HintManager.OnItemMarkedForRelocation(); +#endif void Relocate() { if (item == null || item.Removed) { return; } @@ -1566,7 +1578,7 @@ namespace Barotrauma { HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman", AbortCondition = abortCondition, - allowHoldFire = allowHoldFire, + AllowHoldFire = allowHoldFire, }; if (onAbort != null) { @@ -1583,12 +1595,16 @@ namespace Barotrauma public void SetOrder(Order order, bool speak = true) { objectiveManager.SetOrder(order, speak); +#if CLIENT + HintManager.OnSetOrder(Character, order); +#endif } - public void SetForcedOrder(Order order) + public AIObjective SetForcedOrder(Order order) { var objective = ObjectiveManager.CreateObjective(order); ObjectiveManager.SetForcedOrder(objective); + return objective; } public void ClearForcedOrder() @@ -1677,14 +1693,17 @@ namespace Barotrauma return false; } - public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank); + public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => + HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank); /// - /// Check whether the character has a diving suit in usable condition plus some oxygen. + /// Check whether the character has a diving suit in usable condition, suitable pressure protection for the depth, plus some oxygen. /// - public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) + public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true, bool requireSuitablePressureProtection = true) => HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true, - predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes)); + predicate: (Item item) => + character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes) && + (!requireSuitablePressureProtection || AIObjectiveFindDivingGear.IsSuitablePressureProtection(item, Tags.HeavyDivingGear, character))); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. @@ -1838,7 +1857,7 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f || - otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || + otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI || otherCharacter.IsEscorted || Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f) { continue; @@ -2064,7 +2083,7 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreOxygen = HasDivingGear(character); + bool ignoreOxygen = HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) @@ -2197,11 +2216,29 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { + if (other.IsHusk) + { + // Disguised as husk + return me.IsDisguisedAsHusk; + } + else + { + if (other.IsPrisoner && me.IsPrisoner) + { + // Both prisoners + return true; + } + if (other.IsHostileEscortee && me.IsHostileEscortee) + { + // Both hostile escortees + return true; + } + } bool sameTeam = me.TeamID == other.TeamID; bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { - return other.IsHusk && me.IsDisguisedAsHusk; + return false; } if (other.IsPet) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index ca5aa891e..963912fc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; using System.Linq; +using Voronoi2; namespace Barotrauma { @@ -32,14 +33,21 @@ namespace Barotrauma private Vector2 _attachPos; + /// + /// The character won't latch onto anything when the cooldown is active (activates after the character deattaches for whatever reason). + /// private float attachCooldown; - private Limb attachLimb; + private readonly Limb attachLimb; private Vector2 localAttachPos; - private float attachLimbRotation; + private readonly float attachLimbRotation; private float jointDir; + private float latchedDuration; + + private readonly bool freezeWhenLatched; + public List AttachJoints { get; } = new List(); public Vector2? AttachPos @@ -54,18 +62,19 @@ namespace Barotrauma public LatchOntoAI(XElement element, EnemyAIController enemyAI) { - AttachToWalls = element.GetAttributeBool("attachtowalls", false); - AttachToSub = element.GetAttributeBool("attachtosub", false); - AttachToCharacters = element.GetAttributeBool("attachtocharacters", false); - minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f); - maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f)); - maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f); - coolDown = element.GetAttributeFloat("cooldown", 2f); - damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); - detachStun = element.GetAttributeFloat("detachstun", 0.0f); - localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); - attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f)); - weld = element.GetAttributeBool("weld", true); + AttachToWalls = element.GetAttributeBool(nameof(AttachToWalls), false); + AttachToSub = element.GetAttributeBool(nameof(AttachToSub), false); + AttachToCharacters = element.GetAttributeBool(nameof(AttachToCharacters), false); + minDeattachSpeed = element.GetAttributeFloat(nameof(minDeattachSpeed), 5.0f); + maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat(nameof(maxDeattachSpeed), 8.0f)); + maxAttachDuration = element.GetAttributeFloat(nameof(maxAttachDuration), -1.0f); + coolDown = element.GetAttributeFloat(nameof(coolDown), 2f); + damageOnDetach = element.GetAttributeFloat(nameof(damageOnDetach), 0.0f); + detachStun = element.GetAttributeFloat(nameof(detachStun), 0.0f); + localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2(nameof(localAttachPos), Vector2.Zero)); + attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat(nameof(attachLimbRotation), 0.0f)); + weld = element.GetAttributeBool(nameof(weld), true); + freezeWhenLatched = element.GetAttributeBool(nameof(freezeWhenLatched), false); string limbString = element.GetAttributeString("attachlimb", null); attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase)); @@ -108,7 +117,23 @@ namespace Barotrauma targetBody = target.AnimController.Collider.FarseerBody; attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition); } - + + public void SetAttachTarget(VoronoiCell levelWall) + { + if (!AttachToWalls) { return; } + Reset(); + foreach (Voronoi2.GraphEdge edge in levelWall.Edges) + { + if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, levelWall.Center, out Vector2 intersection)) + { + attachSurfaceNormal = edge.GetNormal(levelWall); + targetBody = levelWall.Body; + _attachPos = ConvertUnits.ToSimUnits(intersection); + return; + } + } + } + public void Update(EnemyAIController enemyAI, float deltaTime) { if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine || @@ -119,6 +144,17 @@ namespace Barotrauma } if (IsAttached) { + latchedDuration += deltaTime; + if (freezeWhenLatched && targetBody is { BodyType: BodyType.Static } && + /*brief delay to let the ragdoll "settle"*/ + latchedDuration > 5.0f) + { + foreach (var limb in character.AnimController.Limbs) + { + limb.body.LinearVelocity = Vector2.Zero; + limb.body.AngularVelocity = 0.0f; + } + } if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir)) { var attachJoint = AttachJoints[0]; @@ -241,7 +277,7 @@ namespace Barotrauma { DeattachFromBody(reset: false); } - else + else if (attachCooldown <= 0.0f) { float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos); float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f; @@ -259,6 +295,10 @@ namespace Barotrauma enemyAI.SteeringManager.SteeringSeek(_attachPos); } } + else if (IsAttached) + { + enemyAI.SteeringManager.Reset(); + } break; case AIState.Attack: case AIState.Aggressive: @@ -281,11 +321,11 @@ namespace Barotrauma if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f) { + attachCooldown = coolDown; bool deattach = false; if (maxAttachDuration > 0) { deattach = true; - attachCooldown = coolDown; } if (!deattach && TargetWall != null && TargetSubmarine != null) { @@ -294,7 +334,6 @@ namespace Barotrauma if (enemyAI.CanPassThroughHole(TargetWall, targetSection)) { deattach = true; - attachCooldown = coolDown; } if (!deattach) { @@ -327,7 +366,7 @@ namespace Barotrauma } } - private void AttachToBody(Vector2 attachPos) + public void AttachToBody(Vector2 attachPos, Vector2? forceAttachSurfaceNormal = null, Vector2? forceColliderSimPosition = null) { if (attachLimb == null) { return; } if (targetBody == null) { return; } @@ -343,6 +382,12 @@ namespace Barotrauma jointDir = attachLimb.Dir; + if (forceAttachSurfaceNormal.HasValue) { attachSurfaceNormal = forceAttachSurfaceNormal.Value; } + if (forceColliderSimPosition.HasValue) + { + character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value)); + } + Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale; if (jointDir < 0.0f) { @@ -350,6 +395,9 @@ namespace Barotrauma } float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir; + //make sure the angle "has the same number of revolutions" as the reference limb + //(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints) + angle = attachLimb.body.WrapAngleToSameNumberOfRevolutions(angle); attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle); var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody, @@ -392,10 +440,26 @@ namespace Barotrauma { deattachCheckTimer = maxAttachDuration; } + +#if SERVER + if (TargetCharacter != null) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetCharacter, attachSurfaceNormal, attachPos)); + } + else if (TargetWall != null) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetWall, attachSurfaceNormal, attachPos)); + } + else if (targetBody.UserData is Voronoi2.VoronoiCell cell) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, cell, attachSurfaceNormal, attachPos)); + } +#endif } public void DeattachFromBody(bool reset, float cooldown = 0) { + bool wasAttached = IsAttached; foreach (Joint joint in AttachJoints) { GameMain.World.Remove(joint); @@ -410,6 +474,12 @@ namespace Barotrauma { Reset(); } +#if SERVER + if (wasAttached) + { + GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData()); + } +#endif } private void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index c9dd03d88..d099b1d21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -294,6 +294,41 @@ namespace Barotrauma return Priority; } + /// + /// Get a normalized value representing how close the target position is. + /// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal. + /// + /// Position of the target + /// How much more costly vertical movement is than horizontal + /// Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be"). + /// The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered). + /// The factor at the minimum distance (= how viable a target that's 0 units a way is considered). + public static float GetDistanceFactor(Vector2 selfPos, Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f) + { + float yDist = Math.Abs(selfPos.Y - targetWorldPos.Y); + yDist = yDist > 100 ? yDist * verticalDistanceMultiplier : 0; + float distance = Math.Abs(selfPos.X - targetWorldPos.X) + yDist; + float distanceFactor = MathHelper.Lerp(factorAtMinDistance, factorAtMaxDistance, MathUtils.InverseLerp(0, maxDistance, distance)); + return + factorAtMinDistance > factorAtMaxDistance ? + MathHelper.Clamp(distanceFactor, factorAtMaxDistance, factorAtMinDistance) : + MathHelper.Clamp(distanceFactor, factorAtMinDistance, factorAtMaxDistance); + } + + /// + /// Get a normalized value representing how close the target position is. + /// The value is a rough estimation, where vertical movement is assumed to be more costly than horizontal. + /// + /// Position of the target + /// How much more costly vertical movement is than horizontal + /// Maximum distance, after which the factor will reach it's minimum value (= anything beyond this point is "as far as it can be"). + /// The factor at the maximum distance and beyond (= how "viable" very far-away targets should be considered). + /// The factor at the minimum distance (= how viable a target that's 0 units a way is considered). + protected float GetDistanceFactor(Vector2 targetWorldPos, float factorAtMaxDistance, float verticalDistanceMultiplier = 3, float maxDistance = 10000.0f, float factorAtMinDistance = 1.0f) + { + return GetDistanceFactor(character.WorldPosition, targetWorldPos, factorAtMaxDistance, verticalDistanceMultiplier, maxDistance, factorAtMinDistance); + } + private void UpdateDevotion(float deltaTime) { var currentObjective = objectiveManager.CurrentObjective; @@ -463,7 +498,7 @@ namespace Barotrauma { hasBeenChecked = true; CheckSubObjectives(); - if (subObjectives.None() || ConcurrentObjectives && subObjectives.All(so => so is AIObjectiveGoTo)) + if (subObjectives.None() || ConcurrentObjectives) { if (Check()) { @@ -509,7 +544,7 @@ namespace Barotrauma public virtual void SpeakAfterOrderReceived() { } - protected static bool CanEquip(Character character, Item item, bool allowWearing) + protected static bool CanPutInInventory(Character character, Item item, bool allowWearing) { if (item == null) { return false; } bool canEquip = false; @@ -550,6 +585,6 @@ namespace Barotrauma return canEquip && character.Inventory.CanBePut(item); } - protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing); + protected bool CanEquip(Item item, bool allowWearing) => CanPutInInventory(character, item, allowWearing); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 45d99f33a..df9c74e24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -21,6 +21,11 @@ namespace Barotrauma private AIObjectiveDecontainItem decontainObjective; private int itemIndex = 0; + /// + /// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup) + /// + public override bool ConcurrentObjectives => true; + public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -39,10 +44,8 @@ namespace Barotrauma float distanceFactor = 0.9f; if (!IsPriority && item.CurrentHull != character.CurrentHull) { - float yDist = Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - item.WorldPosition.X) + yDist; - distanceFactor = MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)); + distanceFactor = GetDistanceFactor(item.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, + factorAtMinDistance: 0.9f, factorAtMaxDistance: 0); } bool isSelected = character.HasItem(item); float selectedBonus = isSelected ? 100 - MaxDevotion : 0; @@ -116,7 +119,7 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { - if (item.IgnoreByAI(character)) + if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item)) { Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 72277a406..8ac1bc914 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -56,8 +56,15 @@ namespace Barotrauma // The validity changes when a character picks the item up. if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); } if (target.CurrentHull.FireSources.Count > 0) { return false; } - // Don't clean up items in rooms that have enemies inside. - if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + foreach (Character c in Character.CharacterList) + { + if (c == character || !HumanAIController.IsActive(c)) { continue; } + if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c)) + { + // Don't clean up items in rooms that have enemies inside. + return false; + } + } return true; } @@ -89,9 +96,10 @@ namespace Barotrauma IsItemInsideValidSubmarine(container, character) && !container.IsClaimedByBallastFlora; - public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) + public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true, bool requireValidContainer = true, bool ignoreItemsMarkedForDeconstruction = true) { if (item == null) { return false; } + if (item.DontCleanUp) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if (item.ParentInventory != null) { @@ -101,8 +109,9 @@ namespace Barotrauma return false; } if (!allowUnloading) { return false; } - if (!IsValidContainer(item.Container, character)) { return false; } + if (requireValidContainer && !IsValidContainer(item.Container, character)) { return false; } } + if (ignoreItemsMarkedForDeconstruction && Item.DeconstructItems.Contains(item)) { return false; } if (!item.HasAccess(character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (item.HasBallastFloraInHull) { return false; } @@ -121,11 +130,16 @@ namespace Barotrauma return false; } } + if (item.GetComponent() is { IsActive: true, Snapped: false }) + { + // Don't clean up spears with an active rope component. + return false; + } if (!checkInventory) { return true; } - return CanEquip(character, item, allowWearing: false); + return CanPutInInventory(character, item, allowWearing: false); } public override void OnDeselected() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 9bbb3836b..a31e90903 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -22,13 +22,13 @@ namespace Barotrauma private readonly CombatMode initialMode; private float checkWeaponsTimer; - private readonly float checkWeaponsInterval = 1; + private const float checkWeaponsInterval = 1; private float ignoreWeaponTimer; - private readonly float ignoredWeaponsClearTime = 10; + private const float ignoredWeaponsClearTime = 10; - private readonly float goodWeaponPriority = 30; + private const float goodWeaponPriority = 30; - private readonly float arrestHoldFireTime = 8; + private const float arrestHoldFireTime = 8; private float holdFireTimer; private bool hasAimed; private bool isLethalWeapon; @@ -79,14 +79,17 @@ namespace Barotrauma private bool canSeeTarget; private float visibilityCheckTimer; - private readonly float visibilityCheckInterval = 0.2f; + private const float visibilityCheckInterval = 0.2f; private float sqrDistance; - private readonly float maxDistance = 2000; - private readonly float distanceCheckInterval = 0.2f; + private const float maxDistance = 2000; + private const float distanceCheckInterval = 0.2f; private float distanceTimer; + + private const float closeDistanceThreshold = 300; + private const float floorHeightApproximate = 100; - public bool allowHoldFire; + public bool AllowHoldFire; /// /// Don't start using a weapon if this condition is true @@ -95,26 +98,63 @@ namespace Barotrauma public enum CombatMode { - Defensive, // Use weapons against the enemy, but try to retreat to a safe place - Offensive, // Engage the enemy and keep attacking it - Arrest, // Try to arrest the enemy without using lethal weapons (stunning + handcuffs) - Retreat, // Run to a safe place without attacking the target - None // Don't use + /// + /// Use weapons against the enemy, but try to retreat to a safe place. + /// + Defensive, + /// + /// Engage the enemy and keep attacking it. + /// + Offensive, + /// + /// Try to arrest the enemy without using lethal weapons (stunning + handcuffs). + /// + Arrest, + /// + /// Attempt to retreat to a safe place. Unlike in the Defensive mode, the character won't try to attack the enemy. + /// + Retreat, + /// + /// Does nothing. + /// + None } public CombatMode Mode { get; private set; } - private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; + private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest; private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator; private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; private float AimSpeed => HumanAIController.AimSpeed; private float AimAccuracy => HumanAIController.AimAccuracy; - private bool IsEnemyCloserThan(float margin) => - Enemy != null && Enemy.CurrentHull != null && - character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin || - HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin; + /// + /// This is just an approximation that attempts to take different rooms and floors into account. + /// It can be equal to a simple distance check, but when the target is nearby, we only use the horizontal axis. + /// It's used for checking whether the enemy is close in certain situations, not for checking the distance to the enemy in general. + /// + private bool IsEnemyClose(float margin) + { + if (Enemy == null) { return false; } + Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition; + if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull) + { + // Inside, not in the same hull with the enemy + if (Math.Abs(toEnemy.Y) > floorHeightApproximate) + { + // Different floor + return false; + } + if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) + { + // Potentially visible and on the same floor -> use only the horizontal distance. + return Math.Abs(toEnemy.X) < margin; + } + } + // Outside or inside in the same hull -> use the normal distance check. + return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin; + } public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) : base(character, objectiveManager, priorityModifier) @@ -147,7 +187,7 @@ namespace Barotrauma protected override float GetPriority() { - if (Enemy == null) + if (Enemy == null || Enemy.Removed) { Priority = 0; Abandon = true; @@ -169,9 +209,9 @@ namespace Barotrauma else { // 91-100 - float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; - float maxPriority = AIObjectiveManager.MaxObjectivePriority; - float priorityScale = maxPriority - minPriority; + const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1; + const float maxPriority = AIObjectiveManager.MaxObjectivePriority; + const float priorityScale = maxPriority - minPriority; float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X); float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y); if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) @@ -208,12 +248,12 @@ namespace Barotrauma ignoredWeapons.Clear(); ignoreWeaponTimer = ignoredWeaponsClearTime; } - bool isCurrentObjective = objectiveManager.IsCurrentObjective(); - if (findSafety != null && isCurrentObjective) + bool isFightingIntruders = objectiveManager.IsCurrentObjective(); + if (findSafety != null && isFightingIntruders) { findSafety.Priority = 0; } - if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective) + if (!AllowCoolDown && !character.IsOnPlayerTeam && !isFightingIntruders) { distanceTimer -= deltaTime; if (distanceTimer < 0) @@ -226,7 +266,7 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { - if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC) + if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC }) { // Can't lose the target in friendly outposts. if (sqrDistance > maxDistance * maxDistance) @@ -343,12 +383,15 @@ namespace Barotrauma RemoveSubObjective(ref seekAmmunitionObjective); return false; } - bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest; + bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons. + (character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items. + && IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons. + && !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something. if (checkWeaponsTimer < 0) { checkWeaponsTimer = checkWeaponsInterval; // First go through all weapons and try to reload without seeking ammunition - var allWeapons = FindWeaponsFromInventory(); + HashSet allWeapons = FindWeaponsFromInventory(); while (allWeapons.Any()) { Weapon = GetWeapon(allWeapons, out _weaponComponent); @@ -369,14 +412,20 @@ namespace Barotrauma // All good, the weapon is loaded break; } - if (Reload(seekAmmo: isAllowedToSeekWeapons)) + bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold); + if (Reload(seekAmmo: seekAmmo)) { // All good, we can use the weapon. break; } + else if (seekAmmunitionObjective != null) + { + // Seeking ammo. + break; + } else { - // No ammo. + // No ammo and should not try to seek ammo. allWeapons.Remove(WeaponComponent); Weapon = null; } @@ -409,16 +458,16 @@ namespace Barotrauma Mode = CombatMode.Retreat; } } - else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority))) + else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold)))) { - // Poor weapon equipped -> try to find better. - RemoveSubObjective(ref seekAmmunitionObjective); + // No weapon or only a poor weapon equipped -> try to find better. RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref followTargetObjective); TryAddSubObjective(ref seekWeaponObjective, constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false) { AllowStealing = HumanAIController.IsMentallyUnstable, + AbortCondition = obj => IsEnemyClose(200), EvaluateCombatPriority = false, // Use a custom formula instead GetItemPriority = i => { @@ -427,7 +476,39 @@ namespace Barotrauma float priority = 0; if (GetWeaponComponent(i) is ItemComponent ic) { - priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100; + priority = GetWeaponPriority(ic, prioritizeMelee: false, canSeekAmmo: true, out _) / 100; + } + if (priority <= 0) { return 0; } + // Check that we are not running directly towards the enemy. + Vector2 toItem = i.WorldPosition - character.WorldPosition; + float range = HumanAIController.FindWeaponsRange; + if (range is > 0 and < float.PositiveInfinity) + { + // Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it. + float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0; + Vector2 adjustedDiff = new Vector2(toItem.X, yDiff); + if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range)) + { + // Too far -> not allowed to seek. + return 0; + } + } + Vector2 toEnemy = Enemy.WorldPosition - character.WorldPosition; + if (Math.Sign(toItem.X) == Math.Sign(toEnemy.X)) + { + // Going towards the enemy -> reduce the priority. + priority *= 0.5f; + } + if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull)) + { + if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate) + { + if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y)) + { + // Different floor, at the direction of the enemy -> reduce the priority. + priority *= 0.75f; + } + } } return priority; } @@ -441,19 +522,19 @@ namespace Barotrauma SpeakNoWeapons(); Mode = CombatMode.Retreat; } - else + else if (!objectiveManager.HasActiveObjective()) { + // Poor weapon equipped Mode = CombatMode.Defensive; } }); } } - else + else if (seekAmmunitionObjective == null && seekWeaponObjective == null) { if (!CheckWeapon(seekAmmo: false)) { Weapon = null; - RemoveSubObjective(ref seekAmmunitionObjective); } } return Weapon != null; @@ -504,10 +585,14 @@ namespace Barotrauma item.GetComponent() ?? item.GetComponent() as ItemComponent; - private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg) + /// + /// Normal range of combat priority is 0-100, but the value is not clamped. + /// + private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool canSeekAmmo, out float lethalDmg) { lethalDmg = -1; float priority = weapon.CombatPriority; + if (priority <= 0) { return 0; } if (weapon is RepairTool repairTool) { switch (repairTool.UsableIn) @@ -531,9 +616,9 @@ namespace Barotrauma } if (weapon.IsEmpty(character)) { - if (weapon is RangedWeapon && isCloseToEnemy) + if (weapon is RangedWeapon && !canSeekAmmo) { - // Ignore weapons that don't have any ammunition (-> Don't seek ammo). + // Ignore weapons that don't have any ammunition, when we are not allowed to seek more ammo. return 0; } else @@ -605,7 +690,45 @@ namespace Barotrauma Attack attack = GetAttackDefinition(weapon); priority = attack?.GetTotalDamage() ?? priority / 2; } + // Reduce the priority of the weapon, if we don't have requires skills to use it. + float startPriority = priority; + var skillRequirementHints = weapon.Item.Prefab.SkillRequirementHints; + if (skillRequirementHints != null) + { + // If there are any skill requirement hints defined, let's use them. + // This should be the most accurate (manually defined) representation of the requirements (taking into account property conditionals etc). + foreach (SkillRequirementHint hint in skillRequirementHints) + { + float skillLevel = character.GetSkillLevel(hint.Skill); + float targetLevel = hint.Level; + priority = ReducePriority(priority, skillLevel, targetLevel); + } + } + else + { + // If no skill requirement hints are defined, let's rely on the required skill definition. + // This can be inaccurate in some cases (hmg, rifle), but in those cases there should be a skill requirement hint defined for the weapon. + foreach (Skill skill in weapon.RequiredSkills) + { + float skillLevel = character.GetSkillLevel(skill.Identifier); + // Skill multiplier is currently always 1, so it's not really needed, but that could change(?) + float targetLevel = skill.Level * weapon.GetSkillMultiplier(); + priority = ReducePriority(priority, skillLevel, targetLevel); + } + } + // Don't allow to reduce more than half, because an assault rifle is still an assault rifle, even in untrained hands. + priority = Math.Max(priority, startPriority / 2); return priority; + + float ReducePriority(float prio, float skillLevel, float targetLevel) + { + float diff = targetLevel - skillLevel; + if (diff > 0) + { + prio -= diff; + } + return prio; + } } private float ApproximateStunDamage(ItemComponent weapon, Attack attack) @@ -632,12 +755,12 @@ namespace Barotrauma return attack.Stun + afflictionsStun + effectsStun; } - private bool CanMeleeStunnerStun(ItemComponent weapon) + private static bool CanMeleeStunnerStun(ItemComponent weapon) { // If there's an item container that takes a battery, // assume that it's required for the stun effect // as we can't check the status effect conditions here. - var mobileBatteryTag = Tags.MobileBattery; + Identifier mobileBatteryTag = Tags.MobileBattery; var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container && container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); @@ -651,11 +774,11 @@ namespace Barotrauma weaponComponent = null; float bestPriority = 0; float lethalDmg = -1; - bool isCloseToEnemy = IsEnemyCloserThan(300); - bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character); + bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character); + bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold); foreach (var weapon in weaponList) { - float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg); + float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg); if (priority > bestPriority) { weaponComponent = weapon; @@ -678,7 +801,7 @@ namespace Barotrauma } isLethalWeapon = lethalDmg > 1; } - if (allowHoldFire && !hasAimed && holdFireTimer <= 0) + if (AllowHoldFire && !hasAimed && holdFireTimer <= 0) { holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f); } @@ -699,15 +822,12 @@ namespace Barotrauma private static Attack GetAttackDefinition(ItemComponent weapon) { - Attack attack = null; - if (weapon is MeleeWeapon meleeWeapon) + Attack attack = weapon switch { - attack = meleeWeapon.Attack; - } - else if (weapon is RangedWeapon rangedWeapon) - { - attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack; - } + MeleeWeapon meleeWeapon => meleeWeapon.Attack, + RangedWeapon rangedWeapon => rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack, + _ => null + }; return attack; } @@ -726,7 +846,7 @@ namespace Barotrauma return weapons; } - private void GetWeapons(Item item, ICollection weaponList) + private static void GetWeapons(Item item, ICollection weaponList) { if (item == null) { return; } foreach (var component in item.Components) @@ -765,14 +885,13 @@ namespace Barotrauma } if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.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); + ClearInputs(); Weapon.TryInteract(character, forceSelectKey: true); - var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s)); + var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType); if (character.Inventory.TryPutItem(Weapon, character, slots)) { SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed); + SetReloadTime(WeaponComponent); } else { @@ -786,7 +905,7 @@ namespace Barotrauma } private float findHullTimer; - private readonly float findHullInterval = 1.0f; + private const float findHullInterval = 1.0f; private void Retreat(float deltaTime) { @@ -796,6 +915,18 @@ namespace Barotrauma } RemoveFollowTarget(); RemoveSubObjective(ref seekAmmunitionObjective); + if (retreatTarget != null) + { + if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) + { + // In the same hull with the enemy + if (retreatTarget == character.CurrentHull) + { + // Go elsewhere + retreatTarget = null; + } + } + } if (retreatObjective != null && retreatObjective.Target != retreatTarget) { RemoveSubObjective(ref retreatObjective); @@ -809,7 +940,7 @@ namespace Barotrauma SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2); return; } - if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) + if (retreatTarget == null || retreatObjective is { CanBeCompleted: false }) { if (findHullTimer > 0) { @@ -942,9 +1073,13 @@ namespace Barotrauma if (!arrestingRegistered && followTargetObjective != null) { followTargetObjective.CloseEnough = - WeaponComponent is RangedWeapon ? 1000 : - WeaponComponent is MeleeWeapon mw ? mw.Range : - WeaponComponent is RepairTool rt ? rt.Range : 50; + WeaponComponent switch + { + RangedWeapon => 1000, + MeleeWeapon mw => mw.Range, + RepairTool rt => rt.Range, + _ => 50 + }; } } @@ -976,9 +1111,8 @@ namespace Barotrauma foreach (var item in Enemy.Inventory.AllItemsMod) { if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound || - item.HasTag(Tags.Weapon) || - item.GetComponent() != null || - item.GetComponent() != null) + item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || + GetWeaponComponent(item) is { CombatPriority: > 0 }) { item.Drop(character); character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot); @@ -1024,10 +1158,11 @@ namespace Barotrauma RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref seekWeaponObjective); RemoveFollowTarget(); + var itemContainer = Weapon.GetComponent(); TryAddSubObjective(ref seekAmmunitionObjective, - constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent(), objectiveManager) + constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager) { - ItemCount = Weapon.GetComponent().Capacity * Weapon.GetComponent().MaxStackSize, + ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize, checkInventory = false, MoveWholeStack = true }, @@ -1052,9 +1187,9 @@ namespace Barotrauma // Eject empty ammo HumanAIController.UnequipEmptyItems(Weapon); ImmutableHashSet ammunitionIdentifiers = null; - if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) + if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { - foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) + foreach (RelatedItem requiredItem in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained]) { if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; } ammunitionIdentifiers = requiredItem.Identifiers; @@ -1075,12 +1210,14 @@ namespace Barotrauma if (ammunition != null) { var container = Weapon.GetComponent(); - if (!container.Inventory.TryPutItem(ammunition, user: character)) + if (container.Inventory.TryPutItem(ammunition, user: character)) { - if (ammunition.ParentInventory == character.Inventory) - { - ammunition.Drop(character); - } + ClearInputs(); + SetReloadTime(WeaponComponent); + } + else if (ammunition.ParentInventory == character.Inventory) + { + ammunition.Drop(character); } } } @@ -1127,7 +1264,7 @@ namespace Barotrauma } if (Weapon.RequireAimToUse) { - character.SetInput(InputType.Aim, false, true); + character.SetInput(InputType.Aim, hit: false, held: true); } hasAimed = true; if (holdFireTimer > 0) @@ -1194,23 +1331,17 @@ namespace Barotrauma float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor) { - if (myBodies == null) - { - myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); - } + myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody); // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); foreach (var body in pickedBodies) { - Character target = null; - if (body.UserData is Character c) + Character target = body.UserData switch { - target = c; - } - else if (body.UserData is Limb limb) - { - target = limb.character; - } + Character c => c, + Limb limb => limb.character, + _ => null + }; if (target != null && target != Enemy && HumanAIController.IsFriendly(target)) { return; @@ -1225,26 +1356,48 @@ namespace Barotrauma { // Never allow to attack characters with deadly weapons while trying to arrest. if (Mode == CombatMode.Arrest && isLethalWeapon) { return; } - float reloadTime = 0; - if (WeaponComponent is RangedWeapon rangedWeapon) - { - // If the weapon is just equipped, we can't shoot just yet. - if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger) - { - reloadTime = rangedWeapon.Reload; - } - } - if (WeaponComponent is MeleeWeapon mw) - { - if (!((HumanoidAnimController)character.AnimController).Crouching) - { - reloadTime = mw.Reload; - } - } - character.SetInput(InputType.Shoot, false, true); + character.SetInput(InputType.Shoot, hit: false, held: true); Weapon.Use(deltaTime, user: character); + SetReloadTime(WeaponComponent); + } + + private float GetReloadTime(ItemComponent weaponComponent) + { + float reloadTime = 0; + switch (weaponComponent) + { + case RangedWeapon rangedWeapon: + { + if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger) + { + reloadTime = rangedWeapon.Reload; + } + break; + } + case MeleeWeapon mw: + { + if (character.AnimController is HumanoidAnimController { Crouching: false }) + { + reloadTime = mw.Reload; + } + break; + } + } + return reloadTime; + } + + private void SetReloadTime(ItemComponent weaponComponent) + { + float reloadTime = GetReloadTime(weaponComponent); reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed); } + + private void ClearInputs() + { + //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); + } private bool ShouldUnequipWeapon => Weapon != null && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs new file mode 100644 index 000000000..80115f5c3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItem.cs @@ -0,0 +1,116 @@ +using Barotrauma.Items.Components; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveDeconstructItem : AIObjective + { + public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier(); + public override bool AllowWhileHandcuffed => false; + + public override bool AllowInFriendlySubs => true; + + public readonly Item Item; + + private Deconstructor deconstructor; + + private AIObjectiveDecontainItem decontainObjective; + + public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + Item = item; + } + + protected override void Act(float deltaTime) + { + if (subObjectives.Any()) { return; } + + if (deconstructor == null) + { + deconstructor = FindDeconstructor(); + if (deconstructor == null) + { + Abandon = true; + return; + } + } + + TryAddSubObjective(ref decontainObjective, + constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager, + sourceContainer: Item.Container?.GetComponent(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier) + { + Equip = true, + RemoveExistingWhenNecessary = true + }, + onCompleted: () => + { + StartDeconstructor(); + //make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost + if (deconstructor.Item.Submarine is { Info.IsOutpost: true }) + { + HumanAIController.HandleRelocation(Item); + deconstructor.RelocateOutputToMainSub = true; + } + IsCompleted = true; + RemoveSubObjective(ref decontainObjective); + }, + onAbandon: () => + { + Abandon = true; + }); + } + + private Deconstructor FindDeconstructor() + { + Deconstructor closestDeconstructor = null; + float bestDistFactor = 0; + foreach (var otherItem in Item.ItemList) + { + var potentialDeconstructor = otherItem.GetComponent(); + if (potentialDeconstructor?.InputContainer == null) { continue; } + if (!potentialDeconstructor.InputContainer.Inventory.CanBePut(Item)) { continue; } + if (!potentialDeconstructor.Item.HasAccess(character)) { continue; } + float distFactor = GetDistanceFactor(Item.WorldPosition, potentialDeconstructor.Item.WorldPosition, factorAtMaxDistance: 0.2f); + if (distFactor > bestDistFactor) + { + closestDeconstructor = potentialDeconstructor; + bestDistFactor = distFactor; + } + } + return closestDeconstructor; + } + + private void StartDeconstructor() + { + deconstructor.SetActive(active: true, user: character, createNetworkEvent: true); + } + + protected override bool CheckObjectiveSpecific() + { + if (Item.IgnoreByAI(character)) + { + Abandon = true; + } + else if (deconstructor != null && deconstructor.Item.IgnoreByAI(character)) + { + Abandon = true; + } + return !Abandon && IsCompleted; + } + + public override void Reset() + { + base.Reset(); + decontainObjective = null; + } + + public void DropTarget() + { + if (Item != null && character.HasItem(Item)) + { + Item.Drop(character); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs new file mode 100644 index 000000000..fbb83b3be --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDeconstructItems.cs @@ -0,0 +1,122 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System.Collections.Generic; + +namespace Barotrauma +{ + class AIObjectiveDeconstructItems : AIObjectiveLoop + { + public override Identifier Identifier { get; set; } = "deconstruct items".ToIdentifier(); + + //Clear periodically, because we may ending up ignoring items when all deconstructors are full + protected override float IgnoreListClearInterval => 30; + + public override bool AllowInFriendlySubs => true; + + protected override int MaxTargets => 10; + + private bool checkedDeconstructorExists; + + public AIObjectiveDeconstructItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + } + + public override void OnSelected() + { + base.OnSelected(); + if (!checkedDeconstructorExists) + { + if (character.Submarine == null || + Item.ItemList.None(it => + it.GetComponent() != null && + it.IsInteractable(character) && + character.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true, allowDifferentTeam: true, allowDifferentType: true))) + { + character.Speak(TextManager.Get("orderdialogself.deconstructitem.nodeconstructor").Value, delay: 5.0f, + identifier: "nodeconstructor".ToIdentifier(), minDurationBetweenSimilar: 30.0f); + Abandon = true; + } + checkedDeconstructorExists = true; + } + } + + public override void Reset() + { + base.Reset(); + checkedDeconstructorExists = false; + } + + protected override float TargetEvaluation() + { + if (Targets.None()) { return 0; } + if (objectiveManager.IsOrder(this)) + { + return objectiveManager.GetOrderPriority(this); + } + return AIObjectiveManager.RunPriority - 0.5f; + } + + protected override bool Filter(Item target) + { + // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. + // The validity changes when a character picks the item up. + if (!IsValidTarget(target, character, checkInventory: true)) + { + return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character); + } + if (target.CurrentHull.FireSources.Count > 0) { return false; } + + foreach (Character c in Character.CharacterList) + { + if (c == character || !HumanAIController.IsActive(c)) { continue; } + if (c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c)) + { + // Don't deconstruct items in rooms that have enemies inside. + return false; + } + else if (c.TeamID == character.TeamID && c.AIController is HumanAIController humanAi) + { + if (humanAi.ObjectiveManager.CurrentObjective is AIObjectiveDeconstructItem deconstruct && deconstruct.Item == target) + { + return false; + } + } + } + return true; + } + + protected override IEnumerable GetList() => Item.DeconstructItems; + + protected override AIObjective ObjectiveConstructor(Item item) + => new AIObjectiveDeconstructItem(item, character, objectiveManager, priorityModifier: PriorityModifier); + + protected override void OnObjectiveCompleted(AIObjective objective, Item target) + => HumanAIController.RemoveTargets(character, target); + + private static bool IsValidTarget(Item item, Character character, bool checkInventory) + { + if (item == null) { return false; } + if (item.GetRootInventoryOwner() == character) { return true; } + return AIObjectiveCleanupItems.IsValidTarget( + item, + character, + checkInventory, + allowUnloading: true, + requireValidContainer: false, + ignoreItemsMarkedForDeconstruction: false); + } + + public override void OnDeselected() + { + base.OnDeselected(); + foreach (var subObjective in SubObjectives) + { + if (subObjective is AIObjectiveDeconstructItem deconstructObjective) + { + deconstructObjective.DropTarget(); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index d8cfdf3fb..f3a5c130c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -39,6 +39,9 @@ namespace Barotrauma /// public bool DropIfFails { get; set; } = true; + /// + /// Should existing item(s) be removed from the targetContainer if the targetItem won't fit otherwise? + /// public bool RemoveExistingWhenNecessary { get; set; } public Func RemoveExistingPredicate { get; set; } public int? RemoveExistingMax { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 7cd20e569..b3d97d251 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -45,13 +45,18 @@ namespace Barotrauma else { float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; - float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); - if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull)) + + float distanceFactor = 1.0f; + if (targetHull != character.CurrentHull && + !HumanAIController.VisibleHulls.Contains(targetHull)) { - distanceFactor = 1; + distanceFactor = + GetDistanceFactor( + new Vector2(character.WorldPosition.Y, characterY), + targetHull.WorldPosition, + verticalDistanceMultiplier: 3, + maxDistance: 5000, + factorAtMaxDistance: 0.1f); } float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull); if (severity > 0.75f && !isOrder && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index a81367582..ad90d4b31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -12,7 +12,7 @@ namespace Barotrauma protected override float TargetUpdateTimeMultiplier => 0.2f; - public bool TargetCharactersInOtherSubs { get; set; } + public bool TargetCharactersInOtherSubs { get; init; } public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } @@ -33,20 +33,22 @@ namespace Barotrauma protected override AIObjective ObjectiveConstructor(Character target) { - AIObjectiveCombat.CombatMode combatMode = ShouldArrest(target, character) ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive; - var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); - if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) + AIObjectiveCombat.CombatMode combatMode = AIObjectiveCombat.CombatMode.Offensive; + if (character.IsOnPlayerTeam && target is { IsEscorted: true }) { - if (campaign.CurrentLocation is { IsFactionHostile: true }) + // Try to arrest escorted characters, instead of killing them. + combatMode = AIObjectiveCombat.CombatMode.Arrest; + } + var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); + if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode { CurrentLocation.IsFactionHostile: true }) + { + combatObjective.holdFireCondition = () => { - combatObjective.holdFireCondition = () => - { - //hold fire while the enemy is in the airlock (except if they've attacked us) - if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; } - return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock"); - }; - character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f); - } + //hold fire while the enemy is in the airlock (except if they've attacked us) + if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; } + return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock"); + }; + character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f); } return combatObjective; } @@ -77,10 +79,5 @@ namespace Barotrauma if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } return true; } - - public static bool ShouldArrest(Character target, Character character) - { - return target != null && target.IsEscorted && character.TeamID == CharacterTeamType.Team1; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 19fb5d725..3cfecafbe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -33,22 +33,27 @@ namespace Barotrauma protected override void Act(float deltaTime) { - TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true)); + TrySetTargetItem(character.Inventory.FindItem(it => it.HasTag(gearTag) && IsSuitablePressureProtection(it, gearTag, character), true)); if (targetItem == null && gearTag == Tags.LightDivingGear) { - TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true)); + TrySetTargetItem(character.Inventory.FindItem( + it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true)); } if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it))) { + bool mustFindMorePressureProtection = + !objectiveManager.FailedToFindDivingGearForDepth && + character.Inventory.FindItem( + it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null; TryAddSubObjective(ref getDivingGear, () => { if (targetItem == null && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f); } - return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) + var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), AllowToFindDivingGear = false, @@ -56,8 +61,42 @@ namespace Barotrauma EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head, Wear = true }; + if (gearTag == Tags.HeavyDivingGear) + { + if (mustFindMorePressureProtection) + { + //if we're looking for a suit specifically because the current suit isn't enough, + //let's ignore unsuitable suits altogether... + getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character); + } + else + { + //...Otherwise it's fine to give a very small priority + //to inadequate suits (a suit not adequate for the depth is better than no suit) + getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f; + } + getItemObjective.GetItemPriority = it => + { + if (IsSuitablePressureProtection(it, gearTag, character)) + { + return 1000.0f; + } + else + { + //if we're looking for a suit specifically because the current suit isn't enough, + //let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority + //to inadequate suits (a suit not adequate for the depth is better than no suit) + return mustFindMorePressureProtection ? 0.0f : 1.0f; + } + }; + } + return getItemObjective; }, - onAbandon: () => Abandon = true, + onAbandon: () => + { + if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; } + Abandon = true; + }, onCompleted: () => { RemoveSubObjective(ref getDivingGear); @@ -160,6 +199,20 @@ namespace Barotrauma } } + public static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character) + { + if (tag == Tags.HeavyDivingGear) + { + float realWorldDepth = Level.Loaded?.GetRealWorldDepth(character.WorldPosition.Y) ?? 0.0f; + if (item.GetComponent() is not { } wearable || wearable.PressureProtection < realWorldDepth + Steering.PressureWarningThreshold) + { + return false; + } + } + return true; + } + + private bool IsSuitableContainedOxygenSource(Item item) { return diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 253dc0e5c..6ef4b1e76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -52,12 +52,26 @@ namespace Barotrauma { bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true); - if (isSuffocatingInDivingSuit || - NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) || - (!objectiveManager.HasActiveObjective() && IsSuffocatingWithoutDivingGear(character))) + + if (isSuffocatingInDivingSuit || (!objectiveManager.HasActiveObjective() && IsSuffocatingWithoutDivingGear(character))) { Priority = AIObjectiveManager.MaxObjectivePriority; } + else if (NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character))) + { + if (objectiveManager.FailedToFindDivingGearForDepth && + HumanAIController.HasDivingSuit(character, requireSuitablePressureProtection: false)) + { + //we have a suit that's not suitable for the pressure, + //but we've failed to find a better one + // shit, not much we can do here, let's just allow the bot to get on with their current objective + Priority = 0; + } + else + { + Priority = AIObjectiveManager.MaxObjectivePriority; + } + } else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID)) { @@ -259,7 +273,7 @@ namespace Barotrauma bool inFriendlySub = character.IsInFriendlySub || (character.IsEscorted && character.IsInPlayerSub); - if (cannotFindSafeHull && !inFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) + if (cannotFindSafeHull && !inFriendlySub && character.IsOnPlayerTeam && objectiveManager.Objectives.None(o => o is AIObjectiveReturn)) { if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab)) { @@ -401,10 +415,7 @@ namespace Barotrauma if (isCharacterInside) { hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character); - float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); + float distanceFactor = GetDistanceFactor(potentialHull.WorldPosition, factorAtMaxDistance: 0.9f); hullSafety *= distanceFactor; //skip the hull if the safety is already less than the best hull //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) @@ -446,16 +457,13 @@ namespace Barotrauma hullSafety = 100; hullIsAirlock = true; } - else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character)) + else if (!bestHullIsAirlock && potentialHull.LeadsOutside(character)) { hullSafety = 100; } float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; - float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 3 : 0; - float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; // Huge preference for closer targets - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance)); + float distanceFactor = GetDistanceFactor(new Vector2(character.WorldPosition.X, characterY), potentialHull.WorldPosition, factorAtMaxDistance: 0.2f); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. // Intentionally exclude wrecks from this check diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs index f6ac0bec8..2d7081475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindThieves.cs @@ -39,8 +39,8 @@ namespace Barotrauma if (campaign.Map?.CurrentLocation?.Reputation is { } reputation) { return MathHelper.Lerp( - campaign.Settings.MaxStolenItemInspectionProbability, - campaign.Settings.MinStolenItemInspectionProbability, + campaign.Settings.PatdownProbabilityMax, + campaign.Settings.PatdownProbabilityMin, reputation.NormalizedValue); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index b4c28bfb7..a065627fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -120,7 +120,7 @@ namespace Barotrauma Abandon = true; return; } - if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained)) + if (weldingTool.OwnInventory == null && repairTool.RequiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained)) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 963be9a2b..2335d8f3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -159,13 +159,14 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (IdentifiersOrTags != null && !isDoneSeeking) + if (IdentifiersOrTags != null) { if (checkInventory) { if (CheckInventory()) { isDoneSeeking = true; + itemCandidates.Clear(); } } if (!isDoneSeeking) @@ -189,7 +190,14 @@ namespace Barotrauma } } FindTargetItem(); - if (!objectiveManager.IsCurrentOrder()) + } + if (targetItem == null) + { + if (isDoneSeeking) + { + HandlePotentialItems(); + } + if (objectiveManager.CurrentOrder is not AIObjectiveGoTo) { objectiveManager.GetObjective().Wander(deltaTime); } @@ -201,20 +209,28 @@ namespace Barotrauma Abandon = true; return; } - if (targetItem == null || targetItem.Removed) + bool ShouldAbort() => IdentifiersOrTags is null || isDoneSeeking && itemCandidates.None(); + if (targetItem is null or { Removed: true }) { + if (ShouldAbort()) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red); #endif - Abandon = true; + Abandon = true; + } return; } - else if (isDoneSeeking && moveToTarget == null) + if (moveToTarget is null) { + if (ShouldAbort()) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red); #endif - Abandon = true; + Abandon = true; + return; + } return; } if (character.IsItemTakenBySomeoneElse(targetItem)) @@ -399,16 +415,8 @@ namespace Barotrauma { StopWatch.Restart(); } - float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100); - if (!CheckPathForEachItem) - { - // While following the player, let's ensure that there's a valid path to the target before accepting it. - // Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can. - // This is relatively expensive, so let's do this only when it significantly improves the behavior. - // Only allow one path find call per frame. - CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder); - } - bool checkPath = CheckPathForEachItem; + float priority = objectiveManager.GetCurrentPriority(); + bool checkPath = CheckPathForEachItem || priority >= AIObjectiveManager.RunPriority || ItemCount > 1; // Reset if the character has switched subs. if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true)) { @@ -434,9 +442,9 @@ namespace Barotrauma // Ignore items in the inventory when defined not to check it. if (item.IsOwnedBy(character)) { continue; } } - if (!AllowStealing) + if (!AllowStealing && character.IsOnPlayerTeam) { - if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInCurrentOutpost) { continue; } + if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; } } if (!CheckItem(item)) { continue; } if (item.Container != null) @@ -454,11 +462,11 @@ namespace Barotrauma if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; } } float itemPriority = item.Prefab.BotPriority; - if (itemPriority <= 0) { continue; } if (GetItemPriority != null) { itemPriority *= GetItemPriority(item); } + if (itemPriority <= 0) { continue; } Entity rootInventoryOwner = item.GetRootInventoryOwner(); if (rootInventoryOwner is Item ownerItem) { @@ -474,11 +482,13 @@ namespace Barotrauma } } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; - float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist; - float minDistFactor = EvaluateCombatPriority ? 0.1f : 0; - float distanceFactor = MathHelper.Lerp(1, minDistFactor, MathUtils.InverseLerp(100, 10000, dist)); + float distanceFactor = + GetDistanceFactor( + itemPos, + verticalDistanceMultiplier: 5, + maxDistance: 10000, + factorAtMinDistance: 1.0f, + factorAtMaxDistance: EvaluateCombatPriority ? 0.1f : 0); itemPriority *= distanceFactor; if (EvaluateCombatPriority) { @@ -510,7 +520,7 @@ namespace Barotrauma } else { - combatFactor = Math.Min(item.Components.Sum(ic => AIObjectiveCombat.GetLethalDamage(ic)) / 1000, 0.1f); + combatFactor = Math.Min(item.Components.Sum(AIObjectiveCombat.GetLethalDamage) / 1000, 0.1f); } itemPriority *= combatFactor; } @@ -518,10 +528,6 @@ namespace Barotrauma { itemPriority *= item.Condition / item.MaxCondition; } - if (checkPath) - { - itemCandidates.Add((item, itemPriority)); - } // Ignore if the item has a lower priority than the currently selected one if (itemPriority < currItemPriority) { continue; } if (EvaluateCombatPriority && itemPriority <= 0) @@ -529,23 +535,27 @@ namespace Barotrauma // Not good enough continue; } - currItemPriority = itemPriority; - targetItem = item; - moveToTarget = rootInventoryOwner ?? item; + if (checkPath) + { + itemCandidates.Add((item, itemPriority)); + } + else + { + currItemPriority = itemPriority; + targetItem = item; + moveToTarget = rootInventoryOwner ?? item; + } } if (currentSearchIndex >= itemList.Count - 1) { isDoneSeeking = true; - } - if (checkedItems > 0) - { - if (isDoneSeeking && itemCandidates.Any()) + if (itemCandidates.Any()) { itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority)); } - if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2) - { - var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}"; + if (HumanAIController.DebugAI && StopWatch.ElapsedMilliseconds > 2) + { + string msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem?.Name ?? "NULL"} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}"; if (StopWatch.ElapsedMilliseconds > 5) { DebugConsole.ThrowError(msg); @@ -557,60 +567,66 @@ namespace Barotrauma } } } - if (isDoneSeeking) + } + + private void HandlePotentialItems() + { + Debug.Assert(isDoneSeeking); + if (itemCandidates.Any()) { if (PathSteering == null) { itemCandidates.Clear(); + Abandon = true; + return; } - if (itemCandidates.Any()) + if (itemCandidates.FirstOrDefault() is var itemCandidate) { - if (itemCandidates.FirstOrDefault() is { } itemCandidate) + var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) { - var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); - if (path.Unreachable) - { - // Remove the invalid candidates and continue on the next frame. - itemCandidates.Remove(itemCandidate); - } - else - { - // The path was valid -> we are done. - itemCandidates.Clear(); - } - } - } - if (targetItem == null && itemCandidates.None()) - { - if (spawnItemIfNotFound) - { - ItemPrefab prefab = FindItemToSpawn(); - if (prefab == null) - { -#if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); -#endif - Abandon = true; - } - else - { - Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => - { - targetItem = spawnedItem; - if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) - { - spawnedItem.SpawnedInCurrentOutpost = true; - } - }); - } + // Remove the invalid candidates and continue on the next frame. + itemCandidates.Remove(itemCandidate); } else { + // The path was valid -> we are done. + itemCandidates.Clear(); + targetItem = itemCandidate.item; + moveToTarget = targetItem.GetRootInventoryOwner() ?? targetItem; + } + } + } + if (targetItem == null) + { + if (spawnItemIfNotFound) + { + ItemPrefab prefab = FindItemToSpawn(); + if (prefab == null) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow); #endif Abandon = true; } + else + { + Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => + { + targetItem = spawnedItem; + if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) + { + spawnedItem.SpawnedInCurrentOutpost = true; + } + }); + } + } + else + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}", Color.Yellow); +#endif + Abandon = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index 5c909a4a0..afd5287e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -3,6 +3,7 @@ using Barotrauma.Extensions; using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; +using System; namespace Barotrauma { @@ -24,6 +25,12 @@ namespace Barotrauma public bool CheckPathForEachItem { get; set; } public bool RequireNonEmpty { get; set; } public bool RequireAllItems { get; set; } + public bool RequireDivingSuitAdequate { get; set; } + + /// + /// T1 = item to check, T2 = tag we're trying to find a suitable item for + /// + public Func? ItemFilter; private readonly ImmutableArray gearTags; private readonly ImmutableHashSet ignoredTags; @@ -48,7 +55,8 @@ namespace Barotrauma int count = gearTags.Count(t => t == tag); AIObjectiveGetItem? getItem = null; TryAddSubObjective(ref getItem, () => - new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) + { + var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1) { AllowVariants = AllowVariants, Wear = Wear, @@ -58,29 +66,36 @@ namespace Barotrauma CheckPathForEachItem = CheckPathForEachItem, RequireNonEmpty = RequireNonEmpty, ItemCount = count, - SpeakIfFails = RequireAllItems - }, - onCompleted: () => + SpeakIfFails = RequireAllItems, + + }; + if (ItemFilter != null) { - var item = getItem?.TargetItem; - if (item?.IsOwnedBy(character) != null) - { - achievedItems.Add(item); - } - }, - onAbandon: () => + getItem.ItemFilter = (Item it) => ItemFilter(it, tag); + } + return getItem; + }, + onCompleted: () => + { + var item = getItem?.TargetItem; + if (item?.IsOwnedBy(character) != null) { - var item = getItem?.TargetItem; - if (item != null) - { - achievedItems.Remove(item); - } - RemoveSubObjective(ref getItem); - if (RequireAllItems) - { - Abandon = true; - } - }); + achievedItems.Add(item); + } + }, + onAbandon: () => + { + var item = getItem?.TargetItem; + if (item != null) + { + achievedItems.Remove(item); + } + RemoveSubObjective(ref getItem); + if (RequireAllItems) + { + Abandon = true; + } + }); } subObjectivesCreated = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index c5b039824..06f4b7b4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -85,6 +85,8 @@ namespace Barotrauma public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } + public bool FaceTargetOnCompleted { get; set; } = true; + public bool AlwaysUseEuclideanDistance { get; set; } = true; /// @@ -324,7 +326,7 @@ namespace Barotrauma float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); if (tryToGetDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); + needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth); } else if (tryToGetDivingGear) { @@ -346,26 +348,26 @@ namespace Barotrauma TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), onAbandon: () => { - cantFindDivingGear = true; - if (needsDivingSuit) - { - // Shouldn't try to reach the target without a suit, because it's lethal. - Abandon = true; - } - else - { - // Try again without requiring the diving suit - RemoveSubObjective(ref findDivingGear); - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => - { - Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); - RemoveSubObjective(ref findDivingGear); - }, - onCompleted: () => - { - RemoveSubObjective(ref findDivingGear); - }); + cantFindDivingGear = true; + if (needsDivingSuit) + { + // Shouldn't try to reach the target without a suit, because it's lethal. + Abandon = true; + } + else + { + // Try again without requiring the diving suit + RemoveSubObjective(ref findDivingGear); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => + { + Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); + RemoveSubObjective(ref findDivingGear); + }, + onCompleted: () => + { + RemoveSubObjective(ref findDivingGear); + }); } }, onCompleted: () => RemoveSubObjective(ref findDivingGear)); @@ -450,10 +452,8 @@ namespace Barotrauma { useScooter = false; checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); - Identifier scooterTag = "scooter".ToIdentifier(); - Identifier batteryTag = "mobilebattery".ToIdentifier(); Item scooter = null; - bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false); + bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false); if (!shouldUseScooter) { float threshold = 500; @@ -467,7 +467,7 @@ namespace Barotrauma shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold; } } - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) + if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) { // Currently equipped scooter scooter = equippedScooters.FirstOrDefault(); @@ -477,23 +477,23 @@ namespace Barotrauma var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); bool handsFull = - (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) || - (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem)); + (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) || + (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable())); if (!handsFull) { bool hasBattery = false; - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false)) + if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false)) { // Non-equipped scooter with a battery scooter = nonEquippedScooters.FirstOrDefault(); hasBattery = true; } - else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable _nonEquippedScooters, requireEquipped: false)) + else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable _nonEquippedScooters, requireEquipped: false)) { // Non-equipped scooter without a battery scooter = _nonEquippedScooters.FirstOrDefault(); // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. - hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); + hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); } if (scooter != null && hasBattery) { @@ -511,7 +511,7 @@ namespace Barotrauma if (scooter.ContainedItems.None(i => i.Condition > 0)) { // Try to switch batteries - if (HumanAIController.HasItem(character, batteryTag, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) + if (HumanAIController.HasItem(character, Tags.MobileBattery, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) { scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot)); if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character)) @@ -811,16 +811,15 @@ namespace Barotrauma private void StopMovement() { SteeringManager?.Reset(); - if (Target != null) + if (FaceTargetOnCompleted && Target is Entity { Removed: false }) { - character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left; + HumanAIController.FaceTarget(Target); } } protected override void OnCompleted() { StopMovement(); - HumanAIController.FaceTarget(Target); if (Target is WayPoint { Ladders: null }) { // Release ladders when ordered to wait at a spawnpoint. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 1e22487b1..4845307ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -454,14 +454,16 @@ namespace Barotrauma { targetHulls.Add(hull); float weight = hull.RectWidth; - // Prefer rooms that are closer. Avoid rooms that are not in the same level. - // If the behavior is active, prefer rooms that are not close. - float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; - float distanceFactor = behavior == BehaviorType.Patrol ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist)) : MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist)); + float distanceFactor = GetDistanceFactor(hull.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 2500, + factorAtMinDistance: 1, factorAtMaxDistance: 0); + if (behavior == BehaviorType.Patrol) + { + //invert when patrolling (= prefer travelling to far-away hulls) + distanceFactor = 1.0f - distanceFactor; + } float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2)); weight *= distanceFactor * waterFactor; + System.Diagnostics.Debug.Assert(weight >= 0); hullWeights.Add(weight); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 74ac238a2..1150c17a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -193,7 +193,10 @@ namespace Barotrauma if (yDist > 100) { dist += yDist * 5; } dist += Math.Abs(character.WorldPosition.X - targetPos.X); } - float distanceFactor = dist > 0.0f ? MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist)) : 0.9f; + + float distanceFactor = + GetDistanceFactor(targetItem.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, factorAtMinDistance: 0.9f, factorAtMaxDistance: 0); + bool hasContainable = character.HasItem(targetItem); float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100; float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 6c4b671f2..5cde9fa68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -67,6 +67,10 @@ namespace Barotrauma } private AIObjective currentOrder; public AIObjective ForcedOrder { get; private set; } + + /// + /// Includes orders. + /// public AIObjective CurrentObjective { get; private set; } public AIObjectiveManager(Character character) @@ -113,6 +117,8 @@ namespace Barotrauma public Dictionary DelayedObjectives { get; private set; } = new Dictionary(); public bool FailedAutonomousObjectives { get; private set; } + public bool FailedToFindDivingGearForDepth; + private void ClearIgnored() { if (character.AIController is HumanAIController humanAi) @@ -229,8 +235,11 @@ namespace Barotrauma if (previousObjective == CurrentObjective) { return CurrentObjective; } previousObjective?.OnDeselected(); - CurrentObjective?.OnSelected(); - GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); + if (CurrentObjective != null) + { + CurrentObjective.OnSelected(); + GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); + } if (GameMain.NetworkMember is { IsServer: true }) { GameMain.NetworkMember.CreateEntityEvent(character, @@ -239,9 +248,14 @@ namespace Barotrauma return CurrentObjective; } + /// + /// Returns the highest priority of the current objective and its subobjectives. + /// public float GetCurrentPriority() { - return CurrentObjective == null ? 0.0f : CurrentObjective.Priority; + if (CurrentObjective == null) { return 0; } + float subObjectivePriority = CurrentObjective.SubObjectives.Any() ? CurrentObjective.SubObjectives.Max(so => so.Priority) : 0; + return Math.Max(CurrentObjective.Priority, subObjectivePriority); } public void UpdateObjectives(float deltaTime) @@ -250,7 +264,7 @@ namespace Barotrauma if (CurrentOrders.Any()) { - foreach(var order in CurrentOrders) + foreach (var order in CurrentOrders) { var orderObjective = order.Objective; UpdateOrderObjective(orderObjective); @@ -405,6 +419,9 @@ namespace Barotrauma } } + //reset this here so the bots can retry finding a better suit if it's needed for the new order + FailedToFindDivingGearForDepth = false; + var newCurrentObjective = CreateObjective(order); if (newCurrentObjective != null) { @@ -601,6 +618,9 @@ namespace Barotrauma case "loaditems": newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier); break; + case "deconstructitems": + newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier); + break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } @@ -622,6 +642,11 @@ namespace Barotrauma return newObjective; } + /// + /// Sets the order as dismissed, and enables the option to reissue the order on the crew list. + /// Note that this is not the same thing as just removing the order entirely! + /// + /// private void DismissSelf(Order order) { var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option)); @@ -660,13 +685,27 @@ namespace Barotrauma return true; } + /// + /// Only checks the current order. Deprecated, use pattern matching instead. + /// public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; + /// + /// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead. + /// public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; - public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); + + /// + /// Return the first order whose objective is of the given type. Can return null. + /// public T GetOrder() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T; + /// + /// Return the first order with the specified objective. Can return null. + /// + public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective); + public T GetLastActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; @@ -674,12 +713,12 @@ namespace Barotrauma => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T; /// - /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. + /// Returns all active objectives of the specific type. /// public IEnumerable GetActiveObjectives() where T : AIObjective { if (CurrentObjective == null) { return Enumerable.Empty(); } - return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); + return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType(); } public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 2e35e6ff0..d9bfcfe0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -211,6 +211,10 @@ namespace Barotrauma return; } } + + //the character shouldn't be grabbing anyone if it's trying to operate an item + character.SelectedCharacter = null; + if (target.CanBeSelected) { if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index b5515d5de..180d0e963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -87,13 +87,29 @@ namespace Barotrauma AIObjectiveGetItems CreateObjectives(IEnumerable itemTags, bool requireAll) { AIObjectiveGetItems objectiveReference = null; - if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags) + if (!TryAddSubObjective(ref objectiveReference, () => { - CheckInventory = CheckInventory, - Equip = Equip, - EvaluateCombatPriority = EvaluateCombatPriority, - RequireNonEmpty = RequireNonEmpty, - RequireAllItems = requireAll + var getItems = new AIObjectiveGetItems(character, objectiveManager, itemTags) + { + CheckInventory = CheckInventory, + Equip = Equip, + EvaluateCombatPriority = EvaluateCombatPriority, + RequireNonEmpty = RequireNonEmpty, + RequireAllItems = requireAll + }; + + if (itemTags.Contains(Tags.HeavyDivingGear)) + { + getItems.ItemFilter = (Item it, Identifier tag) => + { + if (tag == Tags.HeavyDivingGear) + { + return AIObjectiveFindDivingGear.IsSuitablePressureProtection(it, tag, character); + } + return true; + }; + } + return getItems; }, onCompleted: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 81197c7e0..e9594d864 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -64,10 +64,7 @@ namespace Barotrauma float distanceFactor = 1; if (!isPriority && Item.CurrentHull != character.CurrentHull) { - float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; - distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist)); + distanceFactor = GetDistanceFactor(Item.WorldPosition, factorAtMaxDistance: 0.25f, verticalDistanceMultiplier: 5, maxDistance: 4000); } float requiredSuccessFactor = objectiveManager.HasOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100; @@ -113,7 +110,7 @@ namespace Barotrauma if (!repairable.HasRequiredItems(character, false)) { //make sure we have all the items required to fix the target item - foreach (var kvp in repairable.requiredItems) + foreach (var kvp in repairable.RequiredItems) { foreach (RelatedItem requiredItem in kvp.Value) { @@ -140,7 +137,7 @@ namespace Barotrauma } if (repairTool != null) { - if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems)) + if (repairTool.RequiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems)) { if (repairTool.Item.OwnInventory == null) { @@ -282,7 +279,7 @@ namespace Barotrauma { foreach (Repairable repairable in Item.Repairables) { - foreach (var kvp in repairable.requiredItems) + foreach (var kvp in repairable.RequiredItems) { foreach (RelatedItem requiredItem in kvp.Value) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 3e10ab620..1403c14ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -74,7 +74,7 @@ namespace Barotrauma } if (!RelevantSkill.IsEmpty) { - if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; } + if (item.Repairables.None(r => r.RequiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; } } return !HumanAIController.IsItemRepairedByAnother(item, out _); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 14963a008..d62bb4e2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -278,52 +278,66 @@ namespace Barotrauma float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f; - //find which treatments are the most suitable to treat the character's current condition - Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f); - - //check if we already have a suitable treatment for any of the afflictions + float bestSuitability = 0.0f; + Item bestItem = null; + Affliction afflictionToTreat = null; foreach (Affliction affliction in GetSortedAfflictions(Target)) { - if (affliction == null) { throw new Exception("Affliction was null"); } - if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } - float bestSuitability = 0.0f; - Item bestItem = null; - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitabilities) + //find which treatments are the most suitable to treat the character's current condition + Target.CharacterHealth.GetSuitableTreatments( + currentTreatmentSuitabilities, + limb: Target.CharacterHealth.GetAfflictionLimb(affliction), + user: character, + predictFutureDuration: 10.0f); + + foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) { - if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && - currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) + float thisSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key]; + if (thisSuitability <= 0) { continue; } + + Item matchingItem = FindMedicalItem(character.Inventory, treatmentSuitability.Key); + //allow taking items from the target's inventory too if the target is unconscious + if (matchingItem == null && Target.IsIncapacitated) { - Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); - //allow taking items from the target's inventory too if the target is unconscious - if (matchingItem == null && Target.IsIncapacitated) - { - matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true); - } - if (matchingItem != null) - { - bestItem = matchingItem; - bestSuitability = currentTreatmentSuitabilities[treatmentSuitability.Key]; - } + matchingItem = FindMedicalItem(Target.Inventory, treatmentSuitability.Key); } - } - if (bestItem != null) - { - if (Target != character) { character.SelectCharacter(Target); } - ApplyTreatment(affliction, bestItem); - //wait a bit longer after applying a treatment to wait for potential side-effects to manifest - treatmentTimer = TreatmentDelay * 4; - return; + if (matchingItem == null) { continue; } + + //also check how suitable the treatment is for the specific affliction we're now checking + //we don't want to e.g. give fentanyl for oxygen low just because the character has burns on other limbs + //that would also be healed by it! + float suitabilityForThisAffliction = affliction.Prefab.GetTreatmentSuitability(matchingItem); + float totalSuitability = thisSuitability * suitabilityForThisAffliction; + if (matchingItem != null && totalSuitability > bestSuitability) + { + bestItem = matchingItem; + afflictionToTreat = affliction; + bestSuitability = totalSuitability; + } } } + + if (bestItem != null && bestSuitability > cprSuitability) + { + if (Target != character) { character.SelectCharacter(Target); } + ApplyTreatment(afflictionToTreat, bestItem); + //wait a bit longer after applying a treatment to wait for potential side-effects to manifest + treatmentTimer = TreatmentDelay * 4; + return; + } + // Find treatments outside of own inventory only if inside the own sub. if (character.Submarine != null && character.Submarine.TeamID == character.TeamID) { + //get "overall" suitability for no specific limb at this point + Target.CharacterHealth.GetSuitableTreatments( + currentTreatmentSuitabilities, user: character, predictFutureDuration: 10.0f); //didn't have any suitable treatments available, try to find some medical items if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability)) { itemNameList.Clear(); suitableItemIdentifiers.Clear(); - foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) + foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities.OrderByDescending(s => s.Value)) { if (treatmentSuitability.Value <= cprSuitability) { continue; } if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab)) @@ -420,6 +434,28 @@ namespace Barotrauma } } + public static Item FindMedicalItem(Inventory inventory, Identifier itemIdentifier) + { + return FindMedicalItem(inventory, it => it.Prefab.Identifier == itemIdentifier); + } + + public static Item FindMedicalItem(Inventory inventory, Func predicate) + { + if (inventory == null) { return null; } + //prefer items not in a container + Item match = inventory.FindItem(predicate, recursive: false); + if (match != null) { return match; } + + //start from the inventories with most slots + //= prefer taking items from things like toolbelts or doctor's uniforms, as opposed to e.g. autoinjectors which tend to have one or two slots + foreach (var potentialContainer in inventory.AllItems.OrderByDescending(it => it.OwnInventory?.Capacity ?? -1)) + { + match = potentialContainer.OwnInventory?.FindItem(predicate, recursive: true); + if (match != null) { return match; } + } + return null; + } + private void SpeakCannotTreat() { LocalizedString msg = character == Target ? diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 04ee9ccad..9533252c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -122,6 +122,11 @@ namespace Barotrauma public bool HasOptions => Options.Length > 1; public readonly bool MustManuallyAssign; + + /// + /// If enabled and this is an Operate order, it will remove Operate orders of the same item from other characters. + /// If this is a Movement order, removes other Movement orders from the character who receives the order. + /// public readonly bool AutoDismiss; /// @@ -137,7 +142,9 @@ namespace Barotrauma } public OrderTargetType TargetType { get; } public int? WallSectionIndex { get; } - public bool IsIgnoreOrder => Identifier == "ignorethis" || Identifier == "unignorethis"; + public bool IsIgnoreOrder => Identifier == Tags.IgnoreThis || Identifier == Tags.UnignoreThis; + + public bool IsDeconstructOrder => Identifier == Tags.DeconstructThis || Identifier == Tags.DontDeconstructThis; /// /// Should the order icon be drawn when the order target is inside a container @@ -273,7 +280,7 @@ namespace Barotrauma public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs); - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) + public string GetChatMessage(string targetCharacterName, string targetRoomName, Entity targetEntity, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) { if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed") { @@ -304,8 +311,27 @@ namespace Barotrauma } } } + + LocalizedString targetEntityName = string.Empty; + switch (targetEntity) + { + case Item item: + targetEntityName = item.Name; + break; + case Hull hull: + targetEntityName = hull.DisplayName; + break; + case Structure structure: + targetEntityName = structure.Name; + break; + case Character character: + targetEntityName = character.DisplayName; + break; + } + return TextManager.GetWithVariables(messageTag, ("[name]", targetCharacterName ?? string.Empty, FormatCapitals.No), + ("[target]", targetEntityName, FormatCapitals.No), ("[roomname]", targetRoomName ?? string.Empty, FormatCapitals.Yes)).Fallback("").Value; } @@ -413,6 +439,8 @@ namespace Barotrauma public bool TargetItemsMatchItem(Item item, Identifier option = default) { if (item == null) { return false; } + if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct && !Item.DeconstructItems.Contains(item)) { return true; } + if (Identifier == Tags.DontDeconstructThis && Item.DeconstructItems.Contains(item)) { return true; } ImmutableArray targetItems = GetTargetItems(option); return TargetItemsMatchItem(targetItems, item); } @@ -528,6 +556,7 @@ namespace Barotrauma public OrderCategory? Category => Prefab.Category; public bool MustManuallyAssign => Prefab.MustManuallyAssign; public bool IsIgnoreOrder => Prefab.IsIgnoreOrder; + public bool IsDeconstructOrder => Prefab.IsDeconstructOrder; public bool DrawIconWhenContained => Prefab.DrawIconWhenContained; public bool Hidden => Prefab.Hidden; public bool IgnoreAtOutpost => Prefab.IgnoreAtOutpost; @@ -538,7 +567,6 @@ namespace Barotrauma public bool ColoredWhenControllingGiver => Prefab.ColoredWhenControllingGiver; public bool DisplayGiverInTooltip => Prefab.DisplayGiverInTooltip; - public readonly bool UseController; /// @@ -762,7 +790,7 @@ namespace Barotrauma public string GetChatMessage( string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) - => Prefab.GetChatMessage(targetCharacterName, targetRoomName, givingOrderToSelf, orderOption, isNewOrder); + => Prefab.GetChatMessage(targetCharacterName, targetRoomName, TargetEntity, givingOrderToSelf, orderOption, isNewOrder); /// /// Get the target item component based on the target item type diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 874a081bc..bf6db4177 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -52,7 +52,7 @@ namespace Barotrauma { if (orderedCharacter != CommandingCharacter) { - CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); + CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5); } CurrentOrder = SuggestedOrder .WithOption(Option) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index f415c5b96..a500b8178 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -75,7 +75,8 @@ namespace Barotrauma { steering.Y = 0.0f; } - + + /// Update speed for the steering. Should normally match the characters current animation speed. public virtual void Update(float speed) { if (steering == Vector2.Zero || !MathUtils.IsValid(steering)) @@ -86,6 +87,7 @@ namespace Barotrauma } if (steering.LengthSquared() > speed * speed) { + // Can't steer faster than the max speed. steering = Vector2.Normalize(steering) * Math.Abs(speed); } if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index ad724f0bd..958368329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -32,6 +32,48 @@ namespace Barotrauma public abstract GroundedMovementParams RunParams { get; set; } public abstract SwimParams SwimSlowParams { get; set; } public abstract SwimParams SwimFastParams { get; set; } + + protected class AnimSwap + { + public readonly AnimationType AnimationType; + public readonly AnimationParams TemporaryAnimation; + public readonly float Priority; + public bool IsActive + { + get { return _isActive; } + set + { + if (value) + { + expirationTimer = expirationTime; + } + _isActive = value; + } + } + private bool _isActive; + private float expirationTimer; + private const float expirationTime = 0.1f; + + public AnimSwap(AnimationParams temporaryAnimation, float priority) + { + AnimationType = temporaryAnimation.AnimationType; + TemporaryAnimation = temporaryAnimation; + Priority = priority; + IsActive = true; + } + + public void Update(float deltaTime) + { + expirationTimer -= deltaTime; + if (expirationTimer <= 0) + { + IsActive = false; + } + } + } + + protected readonly Dictionary tempAnimations = new Dictionary(); + protected readonly HashSet expiredAnimations = new HashSet(); public AnimationParams CurrentAnimationParams { @@ -87,7 +129,11 @@ namespace Barotrauma } public bool CanWalk => RagdollParams.CanWalk; - public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false }; + public bool IsMovingBackwards => + !InWater && + Math.Sign(targetMovement.X) == -Math.Sign(Dir) && + CurrentAnimationParams is not FishGroundedParams { Flip: false } && + Anim != Animation.Climbing; // TODO: define death anim duration in XML protected float deathAnimTimer, deathAnimDuration = 5.0f; @@ -177,8 +223,14 @@ namespace Barotrauma public float WalkPos { get; protected set; } public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { } + + public void UpdateAnimations(float deltaTime) + { + UpdateTemporaryAnimations(deltaTime); + UpdateAnim(deltaTime); + } - public abstract void UpdateAnim(float deltaTime); + protected abstract void UpdateAnim(float deltaTime); public abstract void DragCharacter(Character target, float deltaTime); @@ -253,23 +305,26 @@ namespace Barotrauma switch (type) { case AnimationType.Walk: - return WalkParams; + return CanWalk ? WalkParams : null; case AnimationType.Run: - return RunParams; + return CanWalk ? RunParams : null; case AnimationType.Crouch: if (this is HumanoidAnimController humanAnimController) { return humanAnimController.HumanCrouchParams; } - throw new NotImplementedException(type.ToString()); + else + { + DebugConsole.ThrowError($"Animation params of type {type} not implemented for non-humanoids!"); + return null; + } case AnimationType.SwimSlow: return SwimSlowParams; case AnimationType.SwimFast: return SwimFastParams; case AnimationType.NotDefined: - return null; default: - throw new NotImplementedException(type.ToString()); + return null; } } @@ -376,7 +431,7 @@ namespace Barotrauma private Direction previousDirection; private readonly Vector2[] transformedHandlePos = new Vector2[2]; //TODO: refactor this method, it's way too convoluted - public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false) + public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 itemPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false, Vector2? targetPos = null) { aimingMelee = aimMelee; if (character.Stun > 0.0f || character.IsIncapacitated) @@ -385,22 +440,20 @@ namespace Barotrauma } //calculate the handle positions - Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); - transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom); - transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom); + Matrix itemTransform = Matrix.CreateRotationZ(item.body.Rotation); + transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransform); + transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransform); Limb torso = GetLimb(LimbType.Torso) ?? MainLimb; Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); - Vector2 itemPos = aim ? aimPos : holdPos; - var controller = character.SelectedItem?.GetComponent(); - bool usingController = controller != null && !controller.AllowAiming; + bool usingController = controller is { AllowAiming: false }; if (!usingController) { controller = character.SelectedSecondaryItem?.GetComponent(); - usingController = controller != null && !controller.AllowAiming; + usingController = controller is { AllowAiming: false }; } bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f; float itemAngle; @@ -408,15 +461,17 @@ namespace Barotrauma float torsoRotation = torso.Rotation; Item rightHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); - bool equippedInRightHand = rightHandItem == item && rightHand != null && !rightHand.IsSevered; + bool equippedInRightHand = rightHandItem == item && rightHand is { IsSevered: false }; Item leftHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand); - bool equippedInLefthand = leftHandItem == item && leftHand != null && !leftHand.IsSevered; + bool equippedInLeftHand = leftHandItem == item && leftHand is { IsSevered: false }; if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated) { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); - Vector2 diff = holdable.Aimable ? - (mousePos - AimSourceSimPos) * Dir : + targetPos ??= ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); + + Vector2 diff = holdable.Aimable ? + (targetPos.Value - AimSourceSimPos) * Dir : MathUtils.RotatePoint(Vector2.UnitX, torsoRotation); + holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir; holdAngle += GetAimWobble(rightHand, leftHand, item); itemAngle = torsoRotation + holdAngle * Dir; @@ -424,7 +479,7 @@ namespace Barotrauma if (holdable.ControlPose) { //if holding two items that should control the characters' pose, let the item in the right hand do it - bool anotherItemControlsPose = equippedInLefthand && rightHandItem != item && (rightHandItem?.GetComponent()?.ControlPose ?? false); + bool anotherItemControlsPose = equippedInLeftHand && rightHandItem != item && (rightHandItem?.GetComponent()?.ControlPose ?? false); if (!anotherItemControlsPose && TargetMovement == Vector2.Zero && inWater) { torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; @@ -441,7 +496,7 @@ namespace Barotrauma { itemAngle = rightHand.Rotation + holdAngle * Dir; } - else if (equippedInLefthand) + else if (equippedInLeftHand) { itemAngle = leftHand.Rotation + holdAngle * Dir; } @@ -465,7 +520,7 @@ namespace Barotrauma transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; itemAngle = rightHand.Rotation + (holdAngle - rightHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; } - else if (equippedInLefthand) + else if (equippedInLeftHand) { transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; itemAngle = leftHand.Rotation + (holdAngle - leftHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; @@ -478,7 +533,7 @@ namespace Barotrauma transformedHoldPos = rightShoulder.WorldAnchorA; rightHand.Disabled = true; } - if (equippedInLefthand) + if (equippedInLeftHand) { if (leftShoulder == null) { return; } transformedHoldPos = leftShoulder.WorldAnchorA; @@ -803,5 +858,260 @@ namespace Barotrauma public void StopUsingItem() => StopAnimation(Animation.UsingItem); public void StopClimbing() => StopAnimation(Animation.Climbing); + + private readonly Dictionary defaultAnimations = new Dictionary(); + + /// + /// Loads an animation (variation) that automatically resets in 0.1s, unless triggered again. + /// Meant e.g. for triggering animations in status effects, without having to worry about resetting them. + /// + public bool TryLoadTemporaryAnimation(StatusEffect.AnimLoadInfo animLoadInfo, bool throwErrors) + { + AnimationType animType = animLoadInfo.Type; + if (tempAnimations.TryGetValue(animType, out AnimSwap animSwap)) + { + if (animLoadInfo.File.TryGet(out string fileName) && animSwap.TemporaryAnimation.FileNameWithoutExtension.Equals(fileName, StringComparison.OrdinalIgnoreCase)) + { + // Already loaded, keep active + animSwap.IsActive = true; + return true; + } + else if (animLoadInfo.File.TryGet(out ContentPath contentPath) && animSwap.TemporaryAnimation.Path == contentPath) + { + // Already loaded, keep active + animSwap.IsActive = true; + return true; + } + else + { + if (animSwap.Priority >= animLoadInfo.Priority) + { + // If the priority of the current animation is higher than the new animation, just return and do nothing. + // Returning false would tell the status effect to not try again, which is not what we want here, which is why we fake a bit with the return value. + return true; + } + else + { + // Override any previous animations of the same type. + tempAnimations.Remove(animType); + } + } + } + AnimationParams defaultAnimation = GetAnimationParamsFromType(animType); + if (defaultAnimation == null) { return false; } + if (!TryLoadAnimation(animType, animLoadInfo.File, out AnimationParams tempParams, throwErrors)) { return false; } + // Store the default animation, if not yet stored. There should always be just one of the same type. + defaultAnimations.TryAdd(animType, defaultAnimation); + tempAnimations.Add(animType, new AnimSwap(tempParams, animLoadInfo.Priority)); + return true; + } + + private void UpdateTemporaryAnimations(float deltaTime) + { + if (tempAnimations.None()) { return; } + foreach ((AnimationType animationType, AnimSwap animSwap) in tempAnimations) + { + if (!animSwap.IsActive) + { + if (defaultAnimations.TryGetValue(animSwap.AnimationType, out AnimationParams defaultAnimation)) + { + TrySwapAnimParams(defaultAnimation); + expiredAnimations.Add(animationType); + } + else + { + DebugConsole.ThrowError($"[AnimController] Failed to find the default animation parameters of type {animSwap.AnimationType}. Cannot swap back the default animations!"); + tempAnimations.Clear(); + } + } + } + foreach (AnimationType anim in expiredAnimations) + { + tempAnimations.Remove(anim); + } + expiredAnimations.Clear(); + foreach (AnimSwap animSwap in tempAnimations.Values) + { + animSwap.Update(deltaTime); + } + } + + /// + /// Loads animations. Non-permanent (= resets on load). + /// + public bool TryLoadAnimation(AnimationType animationType, Either file, out AnimationParams animParams, bool throwErrors) + { + animParams = null; + if (character.IsHumanoid && this is HumanoidAnimController humanAnimController) + { + switch (animationType) + { + case AnimationType.Walk: + humanAnimController.WalkParams = HumanWalkParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.WalkParams; + break; + case AnimationType.Run: + humanAnimController.RunParams = HumanRunParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.RunParams; + break; + case AnimationType.Crouch: + humanAnimController.HumanCrouchParams = HumanCrouchParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.HumanCrouchParams; + break; + case AnimationType.SwimSlow: + humanAnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.SwimSlowParams; + break; + case AnimationType.SwimFast: + humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, file, throwErrors); + animParams = humanAnimController.SwimFastParams; + break; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + break; + } + } + else + { + switch (animationType) + { + case AnimationType.Walk: + if (CanWalk) + { + character.AnimController.WalkParams = FishWalkParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.WalkParams; + } + break; + case AnimationType.Run: + if (CanWalk) + { + character.AnimController.RunParams = FishRunParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.RunParams; + } + break; + case AnimationType.SwimSlow: + character.AnimController.SwimSlowParams = FishSwimSlowParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.SwimSlowParams; + break; + case AnimationType.SwimFast: + character.AnimController.SwimFastParams = FishSwimFastParams.GetAnimParams(character, file, throwErrors); + animParams = character.AnimController.SwimFastParams; + break; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + break; + } + } + + bool success = animParams != null; + if (!file.TryGet(out string fileName)) + { + if (file.TryGet(out ContentPath contentPath)) + { + fileName = contentPath.Value; + if (success) + { + success = contentPath == animParams.Path; + } + } + } + else + { + if (success) + { + success = animParams.FileNameWithoutExtension.Equals(fileName, StringComparison.OrdinalIgnoreCase); + } + } + if (success) + { + DebugConsole.NewMessage($"Animation {fileName} successfully loaded for {character.DisplayName}", Color.LightGreen, debugOnly: true); + } + else if (throwErrors) + { + DebugConsole.ThrowError($"Animation {fileName} for {character.DisplayName} could not be loaded!"); + } + return success; + } + + /// + /// Simply swaps existing animation parameters as current parameters. + /// + protected bool TrySwapAnimParams(AnimationParams newParams) + { + AnimationType animationType = newParams.AnimationType; + if (character.IsHumanoid && this is HumanoidAnimController humanAnimController) + { + switch (animationType) + { + case AnimationType.Walk: + if (newParams is HumanWalkParams newWalkParams) + { + humanAnimController.WalkParams = newWalkParams; + } + return true; + case AnimationType.Run: + if (newParams is HumanRunParams newRunParams) + { + humanAnimController.HumanRunParams = newRunParams; + } + break; + case AnimationType.Crouch: + if (newParams is HumanCrouchParams newCrouchParams) + { + humanAnimController.HumanCrouchParams = newCrouchParams; + } + return true; + case AnimationType.SwimSlow: + if (newParams is HumanSwimSlowParams newSwimSlowParams) + { + humanAnimController.HumanSwimSlowParams = newSwimSlowParams; + } + return true; + case AnimationType.SwimFast: + if (newParams is HumanSwimFastParams newSwimFastParams) + { + humanAnimController.HumanSwimFastParams = newSwimFastParams; + } + return true; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + return false; + } + } + else + { + switch (animationType) + { + case AnimationType.Walk: + if (newParams is FishWalkParams walkParams) + { + character.AnimController.WalkParams = walkParams; + } + return true; + case AnimationType.Run: + if (newParams is FishRunParams runParams) + { + character.AnimController.RunParams = runParams; + } + return true; + case AnimationType.SwimSlow: + if (newParams is FishSwimSlowParams swimSlowParams) + { + character.AnimController.SwimSlowParams = swimSlowParams; + } + return true; + case AnimationType.SwimFast: + if (newParams is FishSwimFastParams swimFastParams) + { + character.AnimController.SwimFastParams = swimFastParams; + } + return true; + default: + DebugConsole.ThrowError($"[AnimController] Animation of type {animationType} not implemented!"); + break; + } + } + return false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 6d726f848..c32e754fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -22,11 +22,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName); - if (!character.VariantOf.IsEmpty) - { - _ragdollParams.ApplyVariantScale(character.Params.VariantFile); - } + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character); } return _ragdollParams; } @@ -133,7 +129,7 @@ namespace Barotrauma public FishAnimController(Character character, string seed, FishRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { } - public override void UpdateAnim(float deltaTime) + protected override void UpdateAnim(float deltaTime) { //wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; } @@ -145,7 +141,7 @@ namespace Barotrauma } var mainLimb = MainLimb; - levitatingCollider = !IsHanging; + levitatingCollider = !IsHangingWithRope; if (!character.CanMove) { @@ -1011,15 +1007,7 @@ namespace Barotrauma { //make sure the angle "has the same number of revolutions" as the reference limb //(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints) - while (referenceLimb.Rotation - angle > MathHelper.TwoPi) - { - angle += MathHelper.TwoPi; - } - while (referenceLimb.Rotation - angle < -MathHelper.TwoPi) - { - angle -= MathHelper.TwoPi; - } - + angle = referenceLimb.body.WrapAngleToSameNumberOfRevolutions(angle); limb?.body.SmoothRotate(angle, torque, wrapAngle: false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 2e3fd26cf..02d15e8b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class HumanoidAnimController : AnimController { - private const float SteepestWalkableSlopeAngleDegrees = 50f; + private const float SteepestWalkableSlopeAngleDegrees = 55f; private const float SlowlyWalkableSlopeAngleDegrees = 30f; private static readonly float SteepestWalkableSlopeNormalX = @@ -36,7 +36,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.SpeciesName); + _ragdollParams = HumanRagdollParams.GetDefaultRagdollParams(character); } return _ragdollParams; } @@ -248,12 +248,12 @@ namespace Barotrauma GetLimb(footType).PullJointLocalAnchorA); } - public override void UpdateAnim(float deltaTime) + protected override void UpdateAnim(float deltaTime) { if (Frozen) { return; } if (MainLimb == null) { return; } - levitatingCollider = !IsHanging; + levitatingCollider = !IsHangingWithRope; if (onGround && character.CanMove) { if ((character.SelectedItem?.GetComponent()?.ControlCharacterPose ?? false) || @@ -396,7 +396,9 @@ namespace Barotrauma if (SimplePhysicsEnabled) { UpdateStandingSimple(); - IsHanging = false; + StopHangingWithRope(); + StopHoldingToRope(); + StopGettingDraggedWithRope(); return; } @@ -490,7 +492,21 @@ namespace Barotrauma aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - IsHanging = IsHanging && character.IsRagdolled; + if (!shouldHangWithRope) + { + StopHangingWithRope(); + } + if (!shouldHoldToRope) + { + StopHoldingToRope(); + } + if (!shouldBeDraggedWithRope) + { + StopGettingDraggedWithRope(); + } + shouldHoldToRope = false; + shouldHangWithRope = false; + shouldBeDraggedWithRope = false; } void UpdateStanding() @@ -686,6 +702,16 @@ namespace Barotrauma if (!onGround) { + const float MaxFootVelocityDiff = 5.0f; + const float MaxFootVelocityDiffSqr = MaxFootVelocityDiff * MaxFootVelocityDiff; + //if the feet have a significantly different velocity from the main limb, try moving them back to a neutral pose below the torso + //this can happen e.g. when jumping over an obstacle: the feet can have a large upwards velocity during the walk/run animation, + //and just "letting go of the animations" would let them keep moving upwards, twisting the character to a weird pose + if ((leftFoot != null && (MainLimb.LinearVelocity - leftFoot.LinearVelocity).LengthSquared() > MaxFootVelocityDiffSqr) || + (rightFoot != null && (MainLimb.LinearVelocity - rightFoot.LinearVelocity).LengthSquared() > MaxFootVelocityDiffSqr)) + { + UpdateFallingProne(10.0f, moveHands: false, moveTorso: false, moveLegs: true); + } return; } @@ -786,7 +812,12 @@ namespace Barotrauma } else { - footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f); + float footPosX = stepSize.X * i * 0.2f; + if (CurrentGroundedParams.StepSizeWhenStanding != Vector2.Zero) + { + footPosX = Math.Sign(stepSize.X) * CurrentGroundedParams.StepSizeWhenStanding.X * i; + } + footPos = new Vector2(colliderPos.X + footPosX, colliderPos.Y - 0.1f); } if (Stairs == null && !onSlopeThatMakesSlow) { @@ -1519,6 +1550,8 @@ namespace Barotrauma { torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + //the pumping animation can sometimes cause impact damage, prevent that by briefly disabling it + target.DisableImpactDamageTimer = 0.15f; cprPumpTimer = 0; if (skill < CPRSettings.Active.DamageSkillThreshold) @@ -1732,7 +1765,7 @@ namespace Barotrauma if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) { pullLimb.PullJointMaxForce = 5000.0f; - if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) + if (!character.CanRunWhileDragging()) { targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); } @@ -1816,7 +1849,7 @@ namespace Barotrauma } //limit movement if moving away from the target - if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging) && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) + if (!character.CanRunWhileDragging() && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) { targetMovement *= MathHelper.Clamp(1.5f - dist, 0.0f, 1.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 628e91e55..6e37ae9da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -71,8 +71,8 @@ namespace Barotrauma public bool Frozen { get { return frozen; } - set - { + set + { if (frozen == value) return; frozen = value; @@ -82,7 +82,7 @@ namespace Barotrauma Collider.FarseerBody.IgnoreGravity = frozen; //Collider.PhysEnabled = !frozen; - if (frozen && MainLimb != null) MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition; + if (frozen && MainLimb != null) { MainLimb.PullJointWorldAnchorB = MainLimb.SimPosition; } } } @@ -110,12 +110,12 @@ namespace Barotrauma //a movement vector that overrides targetmovement if trying to steer //a Character to the position sent by server in multiplayer mode protected Vector2 overrideTargetMovement; - + protected float floorY, standOnFloorY; protected Fixture floorFixture; protected Vector2 floorNormal = Vector2.UnitY; protected float surfaceY; - + protected bool inWater, headInWater; protected bool onGround; public bool OnGround => onGround; @@ -129,7 +129,7 @@ namespace Barotrauma public float ColliderHeightFromFloor => ConvertUnits.ToSimUnits(RagdollParams.ColliderHeightFromFloor) * RagdollParams.JointScale; public Structure Stairs; - + protected Direction dir; public Direction TargetDir; @@ -154,7 +154,7 @@ namespace Barotrauma collider = null; try { - collider = this.collider?[index]; + collider = this.collider?[index]; return true; } catch @@ -323,7 +323,7 @@ namespace Barotrauma impactTolerance = RagdollParams.ImpactTolerance; if (character.Params.VariantFile != null) { - float? tolerance = character.Params.VariantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value); + float? tolerance = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value); if (tolerance.HasValue) { impactTolerance = tolerance; @@ -335,7 +335,8 @@ namespace Barotrauma } public bool Draggable => RagdollParams.Draggable; - public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; + + public CanEnterSubmarine CanEnterSubmarine => RagdollParams.CanEnterSubmarine; public float Dir => dir == Direction.Left ? -1.0f : 1.0f; @@ -385,6 +386,10 @@ namespace Barotrauma if (ragdollParams != null) { RagdollParams = ragdollParams; + if (!character.VariantOf.IsEmpty) + { + RagdollParams.TryApplyVariantScale(character.Params.VariantFile); + } } else { @@ -697,21 +702,34 @@ namespace Barotrauma public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact) { - if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; } - if (f2.UserData is Hull && character.Submarine != null) { return false; } + if (f2.Body.UserData is Submarine submarine && character.Submarine == submarine) { return false; } + if (f2.UserData is Hull) + { + if (character.Submarine != null) + { + return false; + } + if (CanEnterSubmarine == CanEnterSubmarine.Partial) + { + //collider collides with hulls to prevent the character going fully inside the sub, limbs don't + return + f1.Body == Collider.FarseerBody || + (f1.Body.UserData is Limb limb && !limb.Params.CanEnterSubmarine); + } + } //using the velocity of the limb would make the impact damage more realistic, //but would also make it harder to edit the animations because the forces/torques //would all have to be balanced in a way that prevents the character from doing //impact damage to itself Vector2 velocity = Collider.LinearVelocity; - if (character.Submarine == null && f2.Body.UserData is Submarine) + if (character.Submarine == null && f2.Body.UserData is Submarine sub) { - velocity -= ((Submarine)f2.Body.UserData).Velocity; + velocity -= sub.Velocity; } //always collides with bodies other than structures - if (!(f2.Body.UserData is Structure structure)) + if (f2.Body.UserData is not Structure structure) { if (!f2.IsSensor) { @@ -1029,7 +1047,7 @@ namespace Barotrauma { foreach (Ragdoll r in list) { - r.Update(deltaTime, cam); + r.UpdateRagdoll(deltaTime, cam); } } @@ -1049,7 +1067,8 @@ namespace Barotrauma if (newHull == currentHull) { return; } - if (!CanEnterSubmarine || (character.AIController != null && !character.AIController.CanEnterSubmarine)) + if (CanEnterSubmarine == CanEnterSubmarine.False || + (character.AIController != null && character.AIController.CanEnterSubmarine == CanEnterSubmarine.False)) { //character is inside the sub even though it shouldn't be able to enter -> teleport it out @@ -1074,6 +1093,11 @@ namespace Barotrauma } } + if (CanEnterSubmarine != CanEnterSubmarine.True) + { + return; + } + if (setSubmarine) { //in -> out @@ -1083,13 +1107,13 @@ namespace Barotrauma if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; } if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; } character.MemLocalState?.Clear(); - Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity, detachProjectiles: false); + Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity); } //out -> in else if (currentHull == null && newHull.Submarine != null) { character.MemLocalState?.Clear(); - Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity, detachProjectiles: false); + Teleport(-ConvertUnits.ToSimUnits(newHull.Submarine.Position), -newHull.Submarine.Velocity); } //from one sub to another else if (newHull != null && currentHull != null && newHull.Submarine != currentHull.Submarine) @@ -1097,7 +1121,7 @@ namespace Barotrauma character.MemLocalState?.Clear(); Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position; Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position; - Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero, detachProjectiles: false); + Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero); } } @@ -1164,7 +1188,7 @@ namespace Barotrauma character.DisableImpactDamageTimer = 0.25f; - SetPosition(Collider.SimPosition + moveAmount, detachProjectiles: detachProjectiles); + SetPosition(Collider.SimPosition + moveAmount); character.CursorPosition += moveAmount; Collider?.UpdateDrawPosition(); @@ -1229,7 +1253,7 @@ namespace Barotrauma public bool forceStanding; public bool forceNotStanding; - public void Update(float deltaTime, Camera cam) + public void UpdateRagdoll(float deltaTime, Camera cam) { if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; } @@ -1397,7 +1421,8 @@ namespace Barotrauma if (floorNormal.Y is > 0f and < 1f && Math.Sign(movement.X) == Math.Sign(floorNormal.X)) { - slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier; + float steepness = Math.Abs(floorNormal.X); + slopePull = Math.Abs(movement.X * steepness) / LevitationSpeedMultiplier; } if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f) @@ -1751,7 +1776,7 @@ namespace Barotrauma return closestFraction; }, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel); - if (standOnFloorFixture != null && !IsHanging) + if (standOnFloorFixture != null && !IsHangingWithRope) { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; @@ -1853,7 +1878,7 @@ namespace Barotrauma return (surfaceY, ceilingY); } - public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) + public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool moveLatchers = true) { if (!MathUtils.IsValid(simPosition)) { @@ -1866,10 +1891,14 @@ namespace Barotrauma } if (MainLimb == null) { return; } + Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; + // A Work-around for an issue with teleporting the characters: // Detach every latcher when either one of the latchers or the target is teleported, - // because otherwise all the characters are teleported to invalid positions. - if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) + // because otherwise all the characters are teleported to invalid positions. + const float ForceDeattachThreshold = 10.0f; + if (limbMoveAmount.LengthSquared() > ForceDeattachThreshold * ForceDeattachThreshold && + Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) { var target = enemyAI.LatchOntoAI.TargetCharacter; if (target != null) @@ -1882,7 +1911,6 @@ namespace Barotrauma Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true)); Character.Latchers.Clear(); - Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { Collider.TargetPosition = simPosition; @@ -1904,15 +1932,62 @@ namespace Barotrauma } } } + + /// + /// Is attached to something with a rope. + /// + public bool IsHoldingToRope { get; private set; } + protected bool shouldHoldToRope; - public bool IsHanging { get; protected set; } + /// + /// Is hanging to something with a rope, so that can reel towards it. Currently only possible in water. + /// + public bool IsHangingWithRope { get; private set; } + protected bool shouldHangWithRope; + + /// + /// Has someone attached to the character with a rope? + /// + public bool IsDraggedWithRope { get; private set; } + protected bool shouldBeDraggedWithRope; - public void Hang() + public void HangWithRope() { + shouldHangWithRope = true; + IsHangingWithRope = true; ResetPullJoints(); onGround = false; levitatingCollider = false; - IsHanging = true; + } + + public void HoldToRope() + { + shouldHoldToRope = true; + IsHoldingToRope = true; + } + + public void DragWithRope() + { + shouldBeDraggedWithRope = true; + IsDraggedWithRope = true; + } + + protected void StopHangingWithRope() + { + shouldHangWithRope = false; + IsHangingWithRope = false; + } + + protected void StopHoldingToRope() + { + shouldHoldToRope = false; + IsHoldingToRope = false; + } + + protected void StopGettingDraggedWithRope() + { + shouldBeDraggedWithRope = false; + IsDraggedWithRope = false; } protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 8bb5d51ff..92f4d9897 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -37,7 +37,9 @@ namespace Barotrauma FallBackUntilCanAttack, PursueIfCanAttack, Pursue, + Eat, FollowThrough, + FollowThroughWithoutObstacleAvoidance, FollowThroughUntilCanAttack, IdleUntilCanAttack, Reverse, @@ -104,6 +106,13 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes, description: "A delay before reacting after performing an attack."), Editable] public float AfterAttackDelay { get; set; } + [Serialize(AIBehaviorAfterAttack.FallBack, IsPropertySaveable.Yes, + description: "Secondary AI behavior after the attack. The character first executes the AfterAttack behavior, then after AfterAttackSecondaryDelay passes, switches to this one. Ignored if AfterAttackSecondaryDelay is 0 or less."), Editable] + public AIBehaviorAfterAttack AfterAttackSecondary { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How long the character executes the AfterAttack before switching to AfterAttackSecondary. The secondary behavior is ignored if this value is 0 or less."), Editable] + public float AfterAttackSecondaryDelay { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable] public bool Reverse { get; private set; } @@ -135,10 +144,11 @@ namespace Barotrauma [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } - [Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI must wait before it can use this attack again."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float CoolDown { get; set; } = 5; - [Serialize(0f, IsPropertySaveable.Yes, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + + [Serialize(0f, IsPropertySaveable.Yes, description: "When the attack cooldown is running and when there are other valid attacks possible for the character to use, the secondary cooldown is used instead of the regular cooldown. Does not have an effect, if set to 0 or less than the regular cooldown value."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float SecondaryCoolDown { get; set; } = 0; [Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] @@ -155,6 +165,9 @@ namespace Barotrauma set => _structureDamage = value; } + [Serialize(false, IsPropertySaveable.Yes, description: "If the attack causes an explosion of wall damage shrapnel, should some of the shrapnel be launched as projectiles that can go through walls?"), Editable] + public bool CreateWallDamageProjectiles { get; private set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable] public bool EmitStructureDamageParticles { get; private set; } @@ -443,6 +456,12 @@ namespace Barotrauma break; } } + + if (SecondaryCoolDown > CoolDown) + { + DebugConsole.AddWarning($"Potentially misconfigured attack in {parentDebugName}. Secondary cooldown should not be longer than the primary cooldown.", + contentPackage: element.ContentPackage); + } } partial void InitProjSpecific(ContentXElement element); @@ -461,11 +480,6 @@ namespace Barotrauma } affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); - //backwards compatibility - if (subElement.GetAttribute("amount") != null && subElement.GetAttribute("strength") == null) - { - affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f); - } // add the affliction anyway, so that it can be shown in the editor. Afflictions.Add(affliction, subElement); } @@ -708,6 +722,8 @@ namespace Barotrauma public float SecondaryCoolDownTimer { get; set; } public bool IsRunning { get; private set; } + public float AfterAttackTimer { get; set; } + public void UpdateCoolDown(float deltaTime) { CoolDownTimer -= deltaTime; @@ -729,6 +745,7 @@ namespace Barotrauma public void ResetAttackTimer() { + AfterAttackTimer = 0; AttackTimer = 0; IsRunning = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 577047b33..54c753eb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -150,8 +150,8 @@ namespace Barotrauma /// /// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders. /// - 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 IsCommanding => IsPlayer || AIController is HumanAIController { ShipCommandManager.Active: true }; + public bool IsBot => !IsPlayer && AIController is HumanAIController { Enabled: true }; public bool IsEscorted { get; set; } public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty; @@ -352,7 +352,13 @@ namespace Barotrauma public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID == CharacterTeamType.Team2; - public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; + public bool IsInstigator => CombatAction is { IsInstigator: true }; + + /// + /// Set true only, if the character is turned hostile from an escort mission (See ). + /// + public bool IsHostileEscortee; + public CombatAction CombatAction; public readonly AnimController AnimController; @@ -396,8 +402,10 @@ namespace Barotrauma public readonly CharacterPrefab Prefab; public readonly CharacterParams Params; - + public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); + + public Identifier GetBaseCharacterSpeciesName() => Prefab.GetBaseCharacterSpeciesName(SpeciesName); public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; @@ -447,6 +455,17 @@ namespace Barotrauma set => Params.Visibility = value; } + public float MaxPerceptionDistance + { + get => Params.AI?.MaxPerceptionDistance ?? 0; + set + { + if (Params.AI != null) + { + Params.AI.MaxPerceptionDistance = value; + } + } + } public bool IsTraitor { get; @@ -586,9 +605,6 @@ namespace Barotrauma public CharacterInventory Inventory { get; private set; } - private Color speechBubbleColor; - private float speechBubbleTimer; - /// /// Prevents the character from interacting with items or characters /// @@ -688,7 +704,8 @@ namespace Barotrauma if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet }) { #if SERVER - if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings }) + var mpCampaign = GameMain.GameSession.Campaign as MultiPlayerCampaign; + if (mpCampaign != null && GameMain.Server is { ServerSettings: { } settings }) { switch (settings.LootedMoneyDestination) { @@ -701,16 +718,28 @@ namespace Barotrauma } } - + GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money); + + grabbedWallet.Deduct(balance); + //we need to save the grabbed character's wallet this at this point to ensure + //the client doesn't get to keep the money if they respawn + if (mpCampaign != null && selectedCharacter.Info != null) + { + var characterCampaignData = mpCampaign?.GetCharacterData(selectedCharacter.Info); + if (characterCampaignData!= null) + { + characterCampaignData.WalletData = grabbedWallet.Save(); + characterCampaignData?.ApplyWalletData(selectedCharacter); + } + } #elif CLIENT if (GameMain.GameSession.Campaign is SinglePlayerCampaign spCampaign) { spCampaign.Bank.Give(balance); } -#endif - grabbedWallet.Deduct(balance); +#endif } } } @@ -741,6 +770,25 @@ namespace Barotrauma if (item2 != null && item2 != item1) { yield return item2; } } } + + public bool IsDualWieldingRangedWeapons() + { + int rangedItemCount = 0; + foreach (var item in HeldItems) + { + if (item.GetComponent() != null) + { + rangedItemCount++; + } + + if (rangedItemCount > 1) + { + return true; + } + } + + return false; + } private float lowPassMultiplier; public float LowPassMultiplier @@ -847,7 +895,7 @@ namespace Barotrauma public float Stun { - get { return IsRagdolled && !AnimController.IsHanging ? 1.0f : CharacterHealth.Stun; } + get { return IsRagdolled && !AnimController.IsHangingWithRope ? 1.0f : CharacterHealth.Stun; } set { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -1096,7 +1144,7 @@ namespace Barotrauma { get { - return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled; + return (SelectedItem == null || SelectedItem.GetComponent() is { AllowAiming: true }) && !IsIncapacitated && (!IsRagdolled || AnimController.IsHoldingToRope); } } @@ -1356,7 +1404,7 @@ namespace Barotrauma } if (Params.VariantFile != null && Params.MainElement is ContentXElement paramsMainElement) { - var overrideElement = Params.VariantFile.Root.FromPackage(paramsMainElement.ContentPackage); + var overrideElement = Params.VariantFile.GetRootExcludingOverride().FromPackage(paramsMainElement.ContentPackage); // Only override if the override file contains matching elements if (overrideElement.GetChildElement("inventory") != null) { @@ -1438,7 +1486,7 @@ namespace Barotrauma if (ragdollParams == null && prefab.VariantOf == null) { Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; - ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name) : RagdollParams.GetDefaultRagdollParams(name) as RagdollParams; + ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name, Params, Prefab.ContentPackage) : RagdollParams.GetDefaultRagdollParams(name, Params, Prefab.ContentPackage); } if (Params.HasInfo && info == null) { @@ -1825,9 +1873,18 @@ namespace Barotrauma // - dragging someone // - crouching // - moving backwards - public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged || HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) && - (!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) && - !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk); + public bool CanRun => CanRunWhileDragging() && + AnimController is not HumanoidAnimController { Crouching: true } && + !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk) && + !AnimController.IsHoldingToRope; + + public bool CanRunWhileDragging() + { + if (selectedCharacter == null || !selectedCharacter.CanBeDragged) { return true; } + //if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted) + if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; } + return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging); + } public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed) { @@ -2219,6 +2276,25 @@ namespace Barotrauma if (Inventory != null) { + if (IsKeyHit(InputType.DropItem)) + { + foreach (Item item in HeldItems) + { + if (!CanInteractWith(item)) { continue; } + + if (SelectedItem?.OwnInventory != null && SelectedItem.OwnInventory.CanBePut(item)) + { + SelectedItem.OwnInventory.TryPutItem(item, this); + } + else + { + item.Drop(this); + } + //only drop one held item per key hit + break; + } + } + bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) { @@ -2532,7 +2608,7 @@ namespace Barotrauma if (container != null) { if (!container.HasRequiredItems(this, addMessage: false)) { return false; } - if (!container.DrawInventory) { return false; } + if (!container.AllowAccess) { return false; } } } return true; @@ -2594,10 +2670,7 @@ namespace Barotrauma if (itemPriority <= 0) { continue; } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition; - float yDist = Math.Abs(refPos.Y - itemPos.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(refPos.X - itemPos.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist)); + float distanceFactor = AIObjective.GetDistanceFactor(refPos, itemPos, verticalDistanceMultiplier: 5, maxDistance: maxItemDistance, factorAtMaxDistance: 0); itemPriority *= distanceFactor; if (itemPriority > _selectedItemPriority) { @@ -2888,11 +2961,22 @@ namespace Barotrauma //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes aimAssist = 0.0f; } - - focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null; - if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) + + UpdateInteractablesInRange(); + + if (!ShowInteractionLabels) // show labels handles setting the focused item in CharacterHUD, so we can click on them boxes { - FocusedCharacter = null; + focusedItem = CanInteract ? FindClosestItem(interactablesInRange, mouseSimPos, aimAssist) : null; + } + + if (focusedItem != null) + { + if (focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None || + /*pets' "play" interaction can interfere with interacting with items, so let's remove focus from the pet if the cursor is closer to a highlighted item*/ + FocusedCharacter is { IsPet: true } && Vector2.DistanceSquared(focusedItem.SimPosition, mouseSimPos) < Vector2.DistanceSquared(FocusedCharacter.SimPosition, mouseSimPos)) + { + FocusedCharacter = null; + } } findFocusedTimer = 0.05f; } @@ -3075,7 +3159,7 @@ namespace Barotrauma { if (!c.Enabled || c.AnimController.Frozen) continue; - c.AnimController.UpdateAnim(deltaTime); + c.AnimController.UpdateAnimations(deltaTime); } } @@ -3085,7 +3169,7 @@ namespace Barotrauma { foreach (Character c in CharacterList) { - if (!(c is AICharacter) && !c.IsRemotePlayer) continue; + if (c is not AICharacter && !c.IsRemotePlayer) { continue; } if (c.IsPlayer || (c.IsBot && !c.IsDead)) { @@ -3155,8 +3239,14 @@ namespace Barotrauma character.Update(deltaTime, cam); } + +#if CLIENT + UpdateSpeechBubbles(deltaTime); +#endif } + static partial void UpdateSpeechBubbles(float deltaTime); + public virtual void Update(float deltaTime, Camera cam) { UpdateProjSpecific(deltaTime, cam); @@ -3194,8 +3284,6 @@ namespace Barotrauma PreviousHull = CurrentHull; CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, useWorldCoordinates: true); - speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime); - obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f); if (Inventory != null) @@ -3354,11 +3442,14 @@ namespace Barotrauma else if (!tooFastToUnragdoll) { IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; } + if (wasRagdolled != IsRagdolled && !AnimController.IsHangingWithRope) + { + ragdollingLockTimer = 0.2f; + } } SetInput(InputType.Ragdoll, false, IsRagdolled); } - if (!wasRagdolled && IsRagdolled) + if (!wasRagdolled && IsRagdolled && !AnimController.IsHangingWithRope) { CheckTalents(AbilityEffectType.OnRagdoll); } @@ -3618,7 +3709,7 @@ namespace Barotrauma Identifier despawnContainerId = IsHuman ? - "despawncontainer".ToIdentifier() : + Tags.DespawnContainer : Params.DespawnContainer; if (!despawnContainerId.IsEmpty) { @@ -3646,6 +3737,14 @@ namespace Barotrauma var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } List inventoryItems = new List(Inventory.AllItemsMod); + + //unequipping genetic materials normally destroys it in GeneticMaterial.Update, let's do that manually here + var geneticMaterials = Inventory.FindAllItems(it => it.GetComponent() != null, recursive: true); + foreach (var geneticMaterial in geneticMaterials) + { + geneticMaterial.ApplyStatusEffects(ActionType.OnSevered, 1.0f, this); + } + foreach (Item inventoryItem in inventoryItems) { if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents)) @@ -3696,13 +3795,12 @@ namespace Barotrauma float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000); float massFactor = (float)Math.Sqrt(Mass / 20); float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange); + targetRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier); float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed); - newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SightRange = newRange; - } - + } } private void UpdateSoundRange(float deltaTime) @@ -3716,8 +3814,8 @@ namespace Barotrauma { float massFactor = (float)Math.Sqrt(Mass / 10); float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange); + targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed); - newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier); if (!float.IsNaN(newRange)) { aiTarget.SoundRange = newRange; @@ -3994,7 +4092,9 @@ namespace Barotrauma GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this); } #endif - ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.MessageType.Value]); +#if CLIENT + ShowSpeechBubble(ChatMessage.MessageColor[(int)message.MessageType.Value], message.Message); +#endif sentMessages.Add(message); } @@ -4025,13 +4125,6 @@ namespace Barotrauma } } - - public void ShowSpeechBubble(float duration, Color color) - { - speechBubbleTimer = Math.Max(speechBubbleTimer, duration); - speechBubbleColor = color; - } - public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount) { CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount); @@ -4340,8 +4433,8 @@ namespace Barotrauma { if (affliction.Prefab.IsBuff) { continue; } if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; } - if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || - affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + if (Params.Health.ImmunityIdentifiers.Contains(affliction.Identifier)) { continue; } + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { if (!Params.Health.PoisonImmunity) { @@ -4408,7 +4501,7 @@ namespace Barotrauma /// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned. /// With stunning, the parameter uses an one second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property. /// - public bool IsKnockedDown => IsRagdolled || CharacterHealth.StunTimer > 1.0f || IsIncapacitated; + public bool IsKnockedDown => (IsRagdolled && !AnimController.IsHangingWithRope) || CharacterHealth.StunTimer > 1.0f || IsIncapacitated; public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false) { @@ -4621,6 +4714,15 @@ namespace Barotrauma isDead = true; + // Save these resistances in the CharacterInfo object so that if they + // are needed for respawning, they will be available (because there + // will be no Character instance in the limbo/bardo state) + if (info != null) + { + info.LastResistanceMultiplierSkillLossDeath = GetAbilityResistance(Tags.SkillLossDeathResistance); + info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance); + } + ApplyStatusEffects(ActionType.OnDeath, 1.0f); AnimController.Frozen = false; @@ -4629,7 +4731,7 @@ namespace Barotrauma causeOfDeath, causeOfDeathAffliction?.Prefab, causeOfDeathAffliction?.Source, LastDamageSource); - if (GameAnalyticsManager.SendUserStatistics) + if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage) { string causeOfDeathStr = causeOfDeathAffliction == null ? causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", ""); @@ -5116,7 +5218,7 @@ namespace Barotrauma public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure); - #region Talents +#region Talents private readonly List characterTalents = new List(); public IReadOnlyCollection CharacterTalents => characterTalents; @@ -5234,7 +5336,7 @@ namespace Barotrauma partial void OnTalentGiven(TalentPrefab talentPrefab); - #endregion +#endregion private readonly HashSet sameRoomHulls = new(); @@ -5446,6 +5548,24 @@ namespace Barotrauma private readonly Dictionary abilityResistances = new(); + public float GetAbilityResistance(Identifier resistanceId) + { + float resistance = 0f; + bool hadResistance = false; + + foreach (var (key, value) in abilityResistances) + { + if (key.ResistanceIdentifier == resistanceId) + { + resistance += value; + hadResistance = true; + } + } + + // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance + return hadResistance ? resistance : 1f; + } + public float GetAbilityResistance(AfflictionPrefab affliction) { float resistance = 0f; @@ -5461,6 +5581,7 @@ namespace Barotrauma } } + // NOTE: Resistance is handled as a multiplier here, so 1.0 == 0% resistance return hadResistance ? resistance : 1f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 954809cb1..5392608ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Voronoi2; namespace Barotrauma { @@ -27,9 +28,10 @@ namespace Barotrauma UpdateMoney = 13, UpdatePermanentStats = 14, RemoveFromCrew = 15, + LatchOntoTarget = 16, MinValue = 0, - MaxValue = 15 + MaxValue = 16 } private interface IEventData : NetEntityEvent.IData @@ -135,7 +137,56 @@ namespace Barotrauma ObjectiveType = objectiveType; } } - + + public readonly struct LatchedOntoTargetEventData : IEventData + { + public EventType EventType => EventType.LatchOntoTarget; + public readonly bool IsLatched; + public readonly UInt16 TargetCharacterID = NullEntityID; + public readonly UInt16 TargetStructureID = NullEntityID; + public readonly int TargetLevelWallIndex = -1; + + public readonly Vector2 AttachSurfaceNormal = Vector2.Zero; + public readonly Vector2 AttachPos = Vector2.Zero; + + public readonly Vector2 CharacterSimPos; + + private LatchedOntoTargetEventData(Character character, Vector2 attachSurfaceNormal, Vector2 attachPos) + { + CharacterSimPos = character.SimPosition; + IsLatched = true; + AttachSurfaceNormal = attachSurfaceNormal; + AttachPos = attachPos; + } + + public LatchedOntoTargetEventData(Character character, Character targetCharacter, Vector2 attachSurfaceNormal, Vector2 attachPos) + : this(character, attachSurfaceNormal, attachPos) + { + TargetCharacterID = targetCharacter.ID; + } + + public LatchedOntoTargetEventData(Character character, Structure targetStructure, Vector2 attachSurfaceNormal, Vector2 attachPos) + : this(character, attachSurfaceNormal, attachPos) + { + TargetStructureID = targetStructure.ID; + } + + public LatchedOntoTargetEventData(Character character, VoronoiCell levelWall, Vector2 attachSurfaceNormal, Vector2 attachPos) + : this(character, attachSurfaceNormal, attachPos) + { + TargetLevelWallIndex = Level.Loaded.GetAllCells().IndexOf(levelWall); + } + + /// + /// Signifies detaching (not attached to any target) + /// + public LatchedOntoTargetEventData() + { + CharacterSimPos = Vector2.Zero; + IsLatched = false; + } + } + private struct TeamChangeEventData : IEventData { public EventType EventType => EventType.TeamChange; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index d733b8f92..1b673b092 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -490,8 +490,6 @@ namespace Barotrauma public ContentXElement CharacterConfigElement { get; set; } - public readonly string ragdollFileName = string.Empty; - public bool StartItemsGiven; public bool IsNewHire; @@ -553,12 +551,11 @@ namespace Barotrauma { if (ragdoll == null) { - // TODO: support for variants Identifier speciesName = SpeciesName; bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName == CharacterPrefab.HumanSpeciesName); ragdoll = isHumanoid - ? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName) - : RagdollParams.GetRagdollParams(speciesName, ragdollFileName) as RagdollParams; + ? RagdollParams.GetDefaultRagdollParams(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage) + : RagdollParams.GetDefaultRagdollParams(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage); } return ragdoll; } @@ -652,7 +649,6 @@ namespace Barotrauma string name = "", string originalName = "", Either jobOrJobPrefab = null, - string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, Identifier npcIdentifier = default) @@ -703,14 +699,10 @@ namespace Barotrauma Salary = CalculateSalary(); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; - if (ragdollFileName != null) - { - this.ragdollFileName = ragdollFileName; - } } private void SetPersonalityTrait() - => PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet)); + => PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet.OrderBy(tag => tag))); public string GetRandomName(Rand.RandSync randSync) { @@ -832,7 +824,6 @@ namespace Barotrauma StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false); Identifier personalityName = infoElement.GetAttributeIdentifier("personality", ""); - ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty); if (personalityName != Identifier.Empty) { if (NPCPersonalityTrait.Traits.TryGet(personalityName, out var trait) || @@ -1086,7 +1077,26 @@ namespace Barotrauma partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement); + private bool spriteTagsLoaded; + public void VerifySpriteTagsLoaded() + { + if (!spriteTagsLoaded) + { + LoadSpriteTags(); + } + } + private void LoadHeadSprite() + { + LoadHeadElement(loadHeadSprite: true, loadHeadSpriteTags: true); + } + + private void LoadSpriteTags() + { + LoadHeadElement(loadHeadSprite: false, loadHeadSpriteTags: true); + } + + private void LoadHeadElement(bool loadHeadSprite, bool loadHeadSpriteTags) { if (Ragdoll?.MainElement == null) { return; } foreach (var limbElement in Ragdoll.MainElement.Elements()) @@ -1116,20 +1126,30 @@ namespace Barotrauma fileWithoutTags = fileWithoutTags.Split('[', ']').First(); if (fileWithoutTags != fileName) { continue; } - HeadSprite = new Sprite(spriteElement, "", file); - Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; - - //extract the tags out of the filename - SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList(); - if (SpriteTags.Any()) + if (loadHeadSprite) { - SpriteTags.RemoveAt(SpriteTags.Count - 1); + HeadSprite = new Sprite(spriteElement, "", file); + Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + } + + if (loadHeadSpriteTags) + { + //extract the tags out of the filename + SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList(); + if (SpriteTags.Any()) + { + SpriteTags.RemoveAt(SpriteTags.Count - 1); + } + spriteTagsLoaded = true; } break; } - LoadHeadSpriteProjectSpecific(limbElement); + if (loadHeadSprite) + { + LoadHeadSpriteProjectSpecific(limbElement); + } break; } @@ -1446,9 +1466,7 @@ namespace Barotrauma new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)), new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), - new XAttribute("ragdoll", ragdollFileName), new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty)); - // TODO: animations? if (HumanPrefabIds != default) { @@ -1663,8 +1681,7 @@ namespace Barotrauma { Order order = null; string orderIdentifier = orderElement.GetAttributeString("id", ""); - var orderPrefab = OrderPrefab.Prefabs[orderIdentifier]; - if (orderPrefab == null) + if (!OrderPrefab.Prefabs.TryGet(orderIdentifier, out OrderPrefab orderPrefab)) { DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\""); priorityIncrease++; @@ -1968,6 +1985,21 @@ namespace Barotrauma } if (changed) { OnPermanentStatChanged(statType); } } + + /// + /// Used to store the last known resistance against skill loss on death + /// when the character dies, so it can be correctly applied before + /// reinstantiating the Character object (if respawning). + /// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance + /// + public float LastResistanceMultiplierSkillLossDeath = 1.0f; + /// + /// Used to store the last known resistance against skill loss on respawn + /// when the character dies, so it can be correctly applied before + /// reinstantiating the Character object (if respawning). + /// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance + /// + public float LastResistanceMultiplierSkillLossRespawn = 1.0f; } internal sealed class SavedStatValue diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index acae26e4a..c1cea91c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -18,6 +18,19 @@ namespace Barotrauma public string Name => Identifier.Value; public Identifier VariantOf { get; } public CharacterPrefab ParentPrefab { get; set; } + + public Identifier GetBaseCharacterSpeciesName(Identifier speciesName) + { + if (!VariantOf.IsEmpty) + { + speciesName = VariantOf; + if (ParentPrefab is { VariantOf.IsEmpty: false } parentPrefab) + { + speciesName = parentPrefab.GetBaseCharacterSpeciesName(speciesName); + } + } + return speciesName; + } public void InheritFrom(CharacterPrefab parent) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 500f35a65..888f6f892 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -68,7 +68,7 @@ namespace Barotrauma public bool DivideByLimbCount { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable] - public bool MultiplyByMaxVitality { get; private set; } + public bool MultiplyByMaxVitality { get; set; } public float DamagePerSecond; public float DamagePerSecondTimer; @@ -130,11 +130,16 @@ namespace Barotrauma public void Deserialize(XElement element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + //backwards compatibility + if (element.GetAttribute("amount") != null && element.GetAttribute("strength") == null) + { + Strength = element.GetAttributeFloat("amount", 0.0f); + } } public Affliction CreateMultiplied(float multiplier, Affliction affliction) { - var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source); + Affliction instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source); instance.CopyProperties(affliction); return instance; } @@ -183,7 +188,7 @@ namespace Barotrauma if (currentEffect.MultiplyByMaxVitality) { - currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality; + currVitalityDecrease *= characterHealth?.MaxVitality ?? 100.0f; } return currVitalityDecrease; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index c32a364b9..8ae7c3f25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -141,6 +141,9 @@ namespace Barotrauma if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; } if (highestStrength > Strength) { return; } + // Show initial husk warning by default, and disable it only if campaign difficulty settings explicitly disable it + bool showHuskWarning = GameMain.GameSession?.Campaign?.Settings.ShowHuskWarning ?? true; + switch (State) { case InfectionState.Dormant: @@ -148,15 +151,18 @@ namespace Barotrauma { return; } - if (character == Character.Controlled) + if (showHuskWarning) { + if (character == Character.Controlled) + { #if CLIENT - GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); + GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); #endif - } - else if (character.IsBot) - { - character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier()); + } + else if (character.IsBot) + { + character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier()); + } } break; case InfectionState.Transition: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 13b14c30b..da27e83c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -178,6 +178,12 @@ namespace Barotrauma max += Character.Info.Job.Prefab.VitalityModifier; } max *= Character.HumanPrefabHealthMultiplier; + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + max *= Character.IsOnPlayerTeam + ? campaign.Settings.CrewVitalityMultiplier + : campaign.Settings.NonCrewVitalityMultiplier; + } max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier); return max * Character.HealthMultiplier; } @@ -275,9 +281,9 @@ namespace Barotrauma this.Character = character; InitIrremovableAfflictions(); - vitality = UnmodifiedMaxVitality; + vitality = UnmodifiedMaxVitality; - minVitality = character.IsHuman ? -100.0f : 0.0f; + minVitality = element.GetAttributeFloat(nameof(MinVitality), character.IsHuman ? -100.0f : 0.0f); limbHealths.Clear(); limbHealthElement ??= element; @@ -374,7 +380,7 @@ namespace Barotrauma public Limb GetAfflictionLimb(Affliction affliction) { - if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth)) + if (affliction != null && afflictions.TryGetValue(affliction, out LimbHealth limbHealth)) { if (limbHealth == null) { return null; } int limbHealthIndex = limbHealths.IndexOf(limbHealth); @@ -472,13 +478,17 @@ namespace Barotrauma public float GetResistance(AfflictionPrefab afflictionPrefab) { + // This is a % resistance (0 to 1.0) float resistance = 0.0f; foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; resistance += affliction.GetResistance(afflictionPrefab.Identifier); } - return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); + // This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance + float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab); + // The returned value is calculated to be a % resistance again + return 1 - ((1 - resistance) * abilityResistanceMultiplier); } public float GetStatValue(StatTypes statType) @@ -512,7 +522,7 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) { if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } @@ -525,7 +535,7 @@ namespace Barotrauma } } - ReduceMatchingAfflictions(amount, treatmentAction); + ReduceMatchingAfflictions(amount, treatmentAction, attacker); } private IEnumerable GetAfflictionsForLimb(Limb targetLimb) @@ -537,11 +547,11 @@ namespace Barotrauma matchingAfflictions.Clear(); matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); - + ReduceMatchingAfflictions(amount, treatmentAction); } - - public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) + + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null) { if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } @@ -556,14 +566,22 @@ namespace Barotrauma matchingAfflictions.Add(affliction.Key); } } - ReduceMatchingAfflictions(amount, treatmentAction); + ReduceMatchingAfflictions(amount, treatmentAction, attacker); } - private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction) + private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null) { if (matchingAfflictions.Count == 0) { return; } float reduceAmount = amount / matchingAfflictions.Count; + + if (reduceAmount > 0f) + { + var abilityReduceAffliction = new AbilityReduceAffliction(Character, reduceAmount); + attacker?.CheckTalents(AbilityEffectType.OnReduceAffliction, abilityReduceAffliction); + reduceAmount = abilityReduceAffliction.Value; + } + for (int i = matchingAfflictions.Count - 1; i >= 0; i--) { var matchingAffliction = matchingAfflictions[i]; @@ -748,10 +766,16 @@ namespace Barotrauma return; } } - if (Character.Params.Health.PoisonImmunity && - (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; } + if (Character.Params.Health.PoisonImmunity) + { + if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + return; + } + } if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; } if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } + if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; } var should = GameMain.LuaCs.Hook.Call("character.applyAffliction", this, limbHealth, newAffliction, allowStacking); @@ -759,12 +783,11 @@ namespace Barotrauma return; Affliction existingAffliction = null; - foreach (KeyValuePair kvp in afflictions) + foreach ((Affliction affliction, LimbHealth value) in afflictions) { - var affliction = kvp.Key; - if (kvp.Value == limbHealth && kvp.Key.Prefab == newAffliction.Prefab) + if (value == limbHealth && affliction.Prefab == newAffliction.Prefab) { - existingAffliction = kvp.Key; + existingAffliction = affliction; break; } } @@ -990,10 +1013,8 @@ namespace Barotrauma IsParalyzed = false; if (Unkillable || Character.GodMode) { return; } - foreach (KeyValuePair kvp in afflictions) + foreach (var (affliction, limbHealth) in afflictions) { - var affliction = kvp.Key; - var limbHealth = kvp.Value; float vitalityDecrease = affliction.GetVitalityDecrease(this); if (limbHealth != null) { @@ -1133,9 +1154,8 @@ namespace Barotrauma /// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating) /// /// A dictionary where the key is the identifier of the item and the value the suitability - /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. /// If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds. - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) + public void GetSuitableTreatments(Dictionary treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) { //key = item identifier //float = suitability @@ -1145,7 +1165,9 @@ namespace Barotrauma { var affliction = kvp.Key; var limbHealth = kvp.Value; - if (limb != null && affliction.Prefab.IndicatorLimb != limb.type) + if (limb != null && + affliction.Prefab.LimbSpecific && + GetMatchingLimbHealth(affliction) != GetMatchingLimbHealth(limb)) { if (limbHealth == null) { continue; } int healthIndex = limbHealths.IndexOf(limbHealth); @@ -1161,8 +1183,7 @@ namespace Barotrauma //other afflictions of the same type increase the "treatability" // e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false); - if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } - + if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; } if (ignoreHiddenAfflictions) @@ -1180,6 +1201,13 @@ namespace Barotrauma foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitabilities) { float suitability = treatment.Value * strength; + if (suitability > 0) + { + //if this a suitable treatment, ignore it if the affliction isn't severe enough to treat + //if the suitability is negative though, we need to take it into account! + //otherwise we may end up e.g. giving too much opiates to someone already close to overdosing + if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; } + } if (treatment.Value > strength) { //avoid using very effective meds on small injuries @@ -1198,14 +1226,6 @@ namespace Barotrauma maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability); } } - //normalize the suitabilities to a range of 0 to 1 - if (normalize) - { - foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) - { - treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); - } - } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 630e18265..10451960e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -102,6 +102,9 @@ namespace Barotrauma [Serialize(float.PositiveInfinity, IsPropertySaveable.No)] public float ReportRange { get; protected set; } + + [Serialize(float.PositiveInfinity, IsPropertySaveable.No)] + public float FindWeaponsRange { get; protected set; } public Identifier[] PreferredOutpostModuleTypes { get; protected set; } @@ -172,6 +175,7 @@ namespace Barotrauma } } humanAI.ReportRange = ReportRange; + humanAI.FindWeaponsRange = FindWeaponsRange; humanAI.AimSpeed = AimSpeed; humanAI.AimAccuracy = AimAccuracy; } @@ -182,6 +186,7 @@ namespace Barotrauma { humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200) { + FaceTargetOnCompleted = false, DebugLogWhenFails = false, IsWaitOrder = true, CloseEnough = 100 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 52d67296f..b6de960f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -419,6 +419,24 @@ namespace Barotrauma } } + + public Vector2 DrawPosition + { + get + { + if (Removed) + { +#if DEBUG + DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); +#endif + GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:DrawPosition", GameAnalyticsManager.ErrorSeverity.Error, + "Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace()); + return Vector2.Zero; + } + return body.DrawPosition; + } + } + public float Rotation { get @@ -488,6 +506,19 @@ namespace Barotrauma } } + private float _alpha = 1.0f; + /// + /// Can be used by status effects + /// + public float Alpha + { + get => _alpha; + set + { + _alpha = MathHelper.Clamp(value, 0.0f, 1.0f); + } + } + public int RefJointIndex => Params.RefJoint; public readonly List WearingItems = new List(); @@ -680,9 +711,9 @@ namespace Barotrauma } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } - if (character is { VariantOf: { IsEmpty: false } }) + if (character is { VariantOf.IsEmpty: false }) { - var attackElement = character.Params.VariantFile.Root.GetChildElement("attack"); + var attackElement = character.Params.VariantFile.GetRootExcludingOverride().GetChildElement("attack"); if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); @@ -695,7 +726,7 @@ namespace Barotrauma DamageModifiers.Add(new DamageModifier(subElement, character.Name)); break; case "statuseffect": - var statusEffect = StatusEffect.Load(subElement, Name); + var statusEffect = StatusEffect.Load(subElement, character.Name + ", " + Name); if (statusEffect != null) { if (!statusEffects.ContainsKey(statusEffect.type)) @@ -793,10 +824,12 @@ namespace Barotrauma { finalDamageModifier *= character.EmpVulnerability; } - if (!character.Params.Health.PoisonImmunity && - (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) + if (!character.Params.Health.PoisonImmunity) { - finalDamageModifier *= character.PoisonVulnerability; + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) + { + finalDamageModifier *= character.PoisonVulnerability; + } } foreach (DamageModifier damageModifier in tempModifiers) { @@ -908,13 +941,13 @@ namespace Barotrauma { if (Params.BlinkFrequency > 0) { - if (blinkTimer > -TotalBlinkDurationOut) + if (BlinkTimer > -TotalBlinkDurationOut) { - blinkTimer -= deltaTime; + BlinkTimer -= deltaTime; } else { - blinkTimer = Params.BlinkFrequency; + BlinkTimer = Params.BlinkFrequency; } } if (reEnableTimer > 0) @@ -932,13 +965,14 @@ namespace Barotrauma private bool temporarilyDisabled; private float reEnableTimer = -1; + private bool originalIgnoreCollisions; public void HideAndDisable(float duration = 0, bool ignoreCollisions = true) { if (Hidden || Disabled) { return; } - if (ignoreCollisions && IgnoreCollisions) { return; } temporarilyDisabled = true; Hidden = true; Disabled = true; + originalIgnoreCollisions = IgnoreCollisions; IgnoreCollisions = ignoreCollisions; if (duration > 0) { @@ -957,7 +991,7 @@ namespace Barotrauma if (!temporarilyDisabled) { return; } Hidden = false; Disabled = false; - IgnoreCollisions = false; + IgnoreCollisions = originalIgnoreCollisions; reEnableTimer = -1; } @@ -1001,11 +1035,14 @@ namespace Barotrauma case HitDetection.Distance: if (dist < attack.DamageRange) { - structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true, customPredicate: - (Fixture f) => + Vector2 rayStart = simPos; + Vector2 rayEnd = attackSimPos; + if (Submarine == null && damageTarget is ISpatialEntity spatialEntity && spatialEntity.Submarine != null) { - return f?.Body?.UserData as string != "ruinroom"; - }); + rayStart -= spatialEntity.Submarine.SimPosition; + rayEnd -= spatialEntity.Submarine.SimPosition; + } + structureBody = Submarine.CheckVisibility(rayStart, rayEnd); if (damageTarget is Item i && i.GetComponent() != null) { // If the attack is aimed to an item and hits an item, it's successful. @@ -1228,6 +1265,8 @@ namespace Barotrauma if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; } foreach (StatusEffect statusEffect in statusEffectList) { + if (statusEffect.ShouldWaitForInterval(character, deltaTime)) { return; } + statusEffect.sourceBody = body; if (statusEffect.type == ActionType.OnDamaged) { @@ -1308,20 +1347,21 @@ namespace Barotrauma } } - private float blinkTimer; - public float BlinkPhase; + public float BlinkTimer { get; private set; } + public float BlinkPhase { get; set; } + public bool FreezeBlinkState; private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; public void Blink() { - blinkTimer = -TotalBlinkDurationOut; + BlinkTimer = -TotalBlinkDurationOut; } public void UpdateBlink(float deltaTime, float referenceRotation) { - if (blinkTimer > -TotalBlinkDurationOut) + if (BlinkTimer > -TotalBlinkDurationOut) { if (!FreezeBlinkState) { @@ -1431,4 +1471,16 @@ namespace Barotrauma public Affliction Affliction { get; set; } } + class AbilityReduceAffliction : AbilityObject, IAbilityCharacter, IAbilityValue + { + public AbilityReduceAffliction(Character character, float value) + { + Character = character; + Value = value; + } + + public Character Character { get; set; } + public float Value { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index d88356d60..99e289b18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using Barotrauma.IO; using System; +using System.Diagnostics; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; @@ -65,22 +66,19 @@ namespace Barotrauma abstract class AnimationParams : EditableParams, IMemorizable { public Identifier SpeciesName { get; private set; } - public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch; - public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast; + public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch; + public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast; - protected static Dictionary> allAnimations = new Dictionary>(); - /// allAnimations[speciesName][fileName] + /// + /// The cached animations of all the characters that have been loaded. + /// + private static readonly Dictionary> allAnimations = new Dictionary>(); - private float _movementSpeed; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] - public float MovementSpeed - { - get => _movementSpeed; - set => _movementSpeed = value; - } - - [Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), - Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] + public float MovementSpeed { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), + Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float CycleSpeed { get; set; } /// @@ -152,169 +150,214 @@ namespace Barotrauma private static string GetFolder(ContentXElement root, string filePath) { - var folder = root?.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value; + Debug.Assert(filePath != null); + Debug.Assert(root != null); + string folder = root.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value; if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations"); } - return folder.CleanUpPathCrossPlatform(true); + return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } /// - /// Selects a random filepath from multiple paths, matching the specified animation type. + /// Selects all file paths that match the specified animation type and filters them alphabetically. /// - public static string GetRandomFilePath(IReadOnlyList filePaths, AnimationType type) + public static IEnumerable FilterAndSortFiles(IEnumerable filePaths, AnimationType type) { - return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.ServerAndClient); - } - - /// - /// Selects all file paths that match the specified animation type. - /// - public static IEnumerable FilterFilesByType(IEnumerable filePaths, AnimationType type) - { - return filePaths.Where(f => AnimationPredicate(f, type)); - } - - private static bool AnimationPredicate(string filePath, AnimationType type) - { - var doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null) { return false; } - var typeString = doc.Root.GetAttributeString("animationtype", null); - if (string.IsNullOrWhiteSpace(typeString)) + return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase); + + static bool AnimationPredicate(string filePath, AnimationType type) { - typeString = doc.Root.GetAttributeString("AnimationType", "NotDefined"); + XDocument doc = XMLExtensions.TryLoadXml(filePath); + if (doc == null) { return false; } + return doc.GetRootExcludingOverride().GetAttributeEnum("animationtype", AnimationType.NotDefined) == type; } - return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type; } - public static T GetDefaultAnimParams(Character character, AnimationType animType) where T : AnimationParams, new() + protected static T GetDefaultAnimParams(Character character, AnimationType animType) where T : AnimationParams, new() + { + // Using a null file definition means we are taking a first matching file from the folder. + return GetAnimParams(character, animType, file: null, throwErrors: true); + } + + protected static T GetAnimParams(Character character, AnimationType animType, Either file, bool throwErrors = true) where T : AnimationParams, new() { Identifier speciesName = character.SpeciesName; - if (!character.VariantOf.IsEmpty - && (character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeStringUnrestricted("folder", null)).IsNullOrEmpty()) + Identifier animSpecies = speciesName; + if (!character.VariantOf.IsEmpty) { - // Use the base animations defined in the base definition file. - speciesName = character.VariantOf; - } - return GetAnimParams(speciesName, animType, GetDefaultFileName(speciesName, animType)); - } - - /// - /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! - /// If a custom folder is used, it's defined in the character info file. - /// - public static T GetAnimParams(Identifier speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new() - { - if (!allAnimations.TryGetValue(speciesName, out Dictionary anims)) - { - anims = new Dictionary(); - allAnimations.Add(speciesName, anims); - } - if (fileName == null || !anims.TryGetValue(fileName, out AnimationParams anim)) - { - string selectedFile = null; - string folder = GetFolder(speciesName); - if (Directory.Exists(folder)) + string folder = character.Params.VariantFile?.GetRootExcludingOverride().GetChildElement("animations")?.GetAttributeContentPath("folder", character.Prefab.ContentPackage)?.Value; + if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { - var files = Directory.GetFiles(folder); - if (files.None()) + // Use the animations defined in the base definition file. + animSpecies = character.Prefab.GetBaseCharacterSpeciesName(speciesName); + } + } + return GetAnimParams(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors); + } + + private static readonly List errorMessages = new List(); + + private static T GetAnimParams(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either file, bool throwErrors = true) where T : AnimationParams, new() + { + Debug.Assert(!speciesName.IsEmpty); + Debug.Assert(!animSpecies.IsEmpty); + ContentPath contentPath = null; + string fileName = null; + if (file != null) + { + if (!file.TryGet(out fileName)) + { + file.TryGet(out contentPath); + } + Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace()); + } + ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage; + Debug.Assert(contentPackage != null); + if (!allAnimations.TryGetValue(speciesName, out Dictionary animations)) + { + animations = new Dictionary(); + allAnimations.Add(speciesName, animations); + } + string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType); + if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType) + { + // Already cached. + return (T)anim; + } + if (!contentPath.IsNullOrEmpty()) + { + // Load the animation from path. + T animInstance = new T(); + if (animInstance.Load(contentPath, speciesName)) + { + if (animInstance.AnimationType == animType) { - DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation."); - selectedFile = GetDefaultFile(speciesName, animType); - } - var filteredFiles = FilterFilesByType(files, animType); - if (filteredFiles.None()) - { - DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation."); - selectedFile = GetDefaultFile(speciesName, animType); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified. - selectedFile = GetDefaultFile(speciesName, animType); + animations.TryAdd(contentPath.Value, animInstance); + return animInstance; } else { - selectedFile = filteredFiles.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + errorMessages.Add($"[AnimationParams] Animation type mismatch. Expected: {animType}, Actual: {animInstance.AnimationType}. Using the default animation."); + } + } + else + { + errorMessages.Add($"[AnimationParams] Failed to load an animation {animInstance} of type {animType} from {contentPath.Value} for the character {speciesName}. Using the default animation."); + } + } + // Seek the correct animation from the character's animation folder. + string selectedFile = null; + string folder = GetFolder(animSpecies); + if (Directory.Exists(folder)) + { + string[] files = Directory.GetFiles(folder); + if (files.None()) + { + errorMessages.Add($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation."); + } + else + { + var filteredFiles = FilterAndSortFiles(files, animType); + if (filteredFiles.None()) + { + errorMessages.Add($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation."); + } + else if (string.IsNullOrEmpty(fileName)) + { + // Files found, but none specified -> Get a matching animation from the specified folder. + // First try to find a file that matches the default file name. If that fails, just take any file. + string defaultFileName = GetDefaultFileName(animSpecies, animType); + selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First(); + } + else + { + selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName)); if (selectedFile == null) { - DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); - selectedFile = GetDefaultFile(speciesName, animType); + errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); } - } + } } - else - { - DebugConsole.ThrowError($"[Animationparams] Invalid directory: {folder}. Using the default animation."); - selectedFile = GetDefaultFile(speciesName, animType); - } - if (selectedFile == null) - { - throw new Exception("[AnimationParams] Selected file null!"); - } - DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}."); - var characterPrefab = CharacterPrefab.Prefabs[speciesName]; - T a = new T(); - if (a.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) - { - fileName = IO.Path.GetFileNameWithoutExtension(selectedFile); - if (!anims.ContainsKey(fileName)) - { - anims.Add(fileName, a); - } - } - else - { - DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}", - contentPackage: characterPrefab.ContentPackage); - } - return a; } - return (T)anim; + else + { + errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation."); + } + selectedFile ??= GetDefaultFile(fallbackSpecies, animType); + Debug.Assert(selectedFile != null); + if (errorMessages.None()) + { + DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}."); + } + T animationInstance = new T(); + if (animationInstance.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName)) + { + animations.TryAdd(key, animationInstance); + } + else + { + errorMessages.Add($"[AnimationParams] Failed to load an animation {animationInstance} at {selectedFile} of type {animType} for the character {speciesName}"); + } + foreach (string errorMsg in errorMessages) + { + if (throwErrors) + { + DebugConsole.ThrowError(errorMsg, contentPackage: contentPackage); + } + else + { + DebugConsole.Log("Logging a supressed (potential) error: " + errorMsg); + } + } + errorMessages.Clear(); + return animationInstance; + + static bool PathMatchesFile(string p, string f) => IO.Path.GetFileNameWithoutExtension(p).Equals(f, StringComparison.OrdinalIgnoreCase); } public static void ClearCache() => allAnimations.Clear(); - public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type type) + public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType) { - if (type == typeof(HumanWalkParams)) + if (animationParamsType == typeof(HumanWalkParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanRunParams)) + if (animationParamsType == typeof(HumanRunParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanSwimSlowParams)) + if (animationParamsType == typeof(HumanSwimSlowParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanSwimFastParams)) + if (animationParamsType == typeof(HumanSwimFastParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(HumanCrouchParams)) + if (animationParamsType == typeof(HumanCrouchParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishWalkParams)) + if (animationParamsType == typeof(FishWalkParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishRunParams)) + if (animationParamsType == typeof(FishRunParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishSwimSlowParams)) + if (animationParamsType == typeof(FishSwimSlowParams)) { return Create(fullPath, speciesName, animationType); } - if (type == typeof(FishSwimFastParams)) + if (animationParamsType == typeof(FishSwimFastParams)) { return Create(fullPath, speciesName, animationType); } - throw new NotImplementedException(type.ToString()); + throw new NotImplementedException(animationParamsType.ToString()); } /// @@ -331,7 +374,7 @@ namespace Barotrauma anims = new Dictionary(); allAnimations.Add(speciesName, anims); } - var fileName = IO.Path.GetFileNameWithoutExtension(fullPath); + string fileName = IO.Path.GetFileNameWithoutExtension(fullPath); if (anims.ContainsKey(fileName)) { DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red); @@ -340,7 +383,8 @@ namespace Barotrauma var instance = new T(); XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString())); instance.doc = new XDocument(animationElement); - var characterPrefab = CharacterPrefab.Prefabs[speciesName]; + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + Debug.Assert(characterPrefab != null); var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath); instance.UpdatePath(contentPath); instance.IsLoaded = instance.Deserialize(animationElement); @@ -373,16 +417,17 @@ namespace Barotrauma else { // Update the key by removing and re-adding the animation. + string fileName = FileNameWithoutExtension; if (allAnimations.TryGetValue(SpeciesName, out Dictionary animations)) { - animations.Remove(Name); + animations.Remove(fileName); } base.UpdatePath(newPath); if (animations != null) { - if (!animations.ContainsKey(Name)) + if (!animations.ContainsKey(fileName)) { - animations.Add(Name, this); + animations.Add(fileName, this); } } } @@ -421,37 +466,26 @@ namespace Barotrauma { if (isHumanoid) { - switch (type) + return type switch { - case AnimationType.Walk: - return typeof(HumanWalkParams); - case AnimationType.Run: - return typeof(HumanRunParams); - case AnimationType.Crouch: - return typeof(HumanCrouchParams); - case AnimationType.SwimSlow: - return typeof(HumanSwimSlowParams); - case AnimationType.SwimFast: - return typeof(HumanSwimFastParams); - default: - throw new NotImplementedException(type.ToString()); - } + AnimationType.Walk => typeof(HumanWalkParams), + AnimationType.Run => typeof(HumanRunParams), + AnimationType.Crouch => typeof(HumanCrouchParams), + AnimationType.SwimSlow => typeof(HumanSwimSlowParams), + AnimationType.SwimFast => typeof(HumanSwimFastParams), + _ => throw new NotImplementedException(type.ToString()) + }; } else { - switch (type) + return type switch { - case AnimationType.Walk: - return typeof(FishWalkParams); - case AnimationType.Run: - return typeof(FishRunParams); - case AnimationType.SwimSlow: - return typeof(FishSwimSlowParams); - case AnimationType.SwimFast: - return typeof(FishSwimFastParams); - default: - throw new NotImplementedException(type.ToString()); - } + AnimationType.Walk => typeof(FishWalkParams), + AnimationType.Run => typeof(FishRunParams), + AnimationType.SwimSlow => typeof(FishSwimSlowParams), + AnimationType.SwimFast => typeof(FishSwimFastParams), + _ => throw new NotImplementedException(type.ToString()) + }; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 6a5c7aab6..4962b96dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -9,12 +9,12 @@ namespace Barotrauma { return Check(character) ? GetDefaultAnimParams(character, AnimationType.Walk) : Empty; } - public static FishWalkParams GetAnimParams(Character character, string fileName = null) + public static FishWalkParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName) : Empty; + return Check(character) ? GetAnimParams(character, AnimationType.Walk, file, throwErrors) : null; } - protected static FishWalkParams Empty = new FishWalkParams(); + protected static readonly FishWalkParams Empty = new FishWalkParams(); public override void StoreSnapshot() => StoreSnapshot(); } @@ -25,12 +25,12 @@ namespace Barotrauma { return Check(character) ? GetDefaultAnimParams(character, AnimationType.Run) : Empty; } - public static FishRunParams GetAnimParams(Character character, string fileName = null) + public static FishRunParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Run, fileName) : Empty; + return Check(character) ? GetAnimParams(character, AnimationType.Run, file, throwErrors) : null; } - protected static FishRunParams Empty = new FishRunParams(); + protected static readonly FishRunParams Empty = new FishRunParams(); public override void StoreSnapshot() => StoreSnapshot(); } @@ -38,9 +38,9 @@ namespace Barotrauma class FishSwimFastParams : FishSwimParams { public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); - public static FishSwimFastParams GetAnimParams(Character character, string fileName = null) + public static FishSwimFastParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); + return GetAnimParams(character, AnimationType.SwimFast, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -49,9 +49,9 @@ namespace Barotrauma class FishSwimSlowParams : FishSwimParams { public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); - public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null) + public static FishSwimSlowParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); + return GetAnimParams(character, AnimationType.SwimSlow, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index a1920a820..48981573b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -5,9 +5,9 @@ namespace Barotrauma class HumanWalkParams : HumanGroundedParams { public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Walk); - public static HumanWalkParams GetAnimParams(Character character, string fileName = null) + public static HumanWalkParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName); + return GetAnimParams(character, AnimationType.Walk, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -16,9 +16,9 @@ namespace Barotrauma class HumanRunParams : HumanGroundedParams { public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Run); - public static HumanRunParams GetAnimParams(Character character, string fileName = null) + public static HumanRunParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.Run, fileName); + return GetAnimParams(character, AnimationType.Run, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -36,9 +36,9 @@ namespace Barotrauma public float ExtraTorsoAngleWhenStationary { get; set; } public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); - public static HumanCrouchParams GetAnimParams(Character character, string fileName = null) + public static HumanCrouchParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.Crouch, fileName); + return GetAnimParams(character, AnimationType.Crouch, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -47,9 +47,9 @@ namespace Barotrauma class HumanSwimFastParams: HumanSwimParams { public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); - public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null) + public static HumanSwimFastParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); + return GetAnimParams(character, AnimationType.SwimFast, file, throwErrors); } @@ -59,9 +59,9 @@ namespace Barotrauma class HumanSwimSlowParams : HumanSwimParams { public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); - public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null) + public static HumanSwimSlowParams GetAnimParams(Character character, Either file, bool throwErrors = true) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); + return GetAnimParams(character, AnimationType.SwimSlow, file, throwErrors); } public override void StoreSnapshot() => StoreSnapshot(); @@ -125,6 +125,13 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)] public float FootLiftHorizontalFactor { get; set; } + [Serialize("0,0", IsPropertySaveable.Yes, description: "Normally the character's feet are positioned at a scaled-down version of it's normal step position - this can be used to override that value if you want to e.g. make the character to spread out it's feet more when standing."), Editable(DecimalCount = 2, ValueStep = 0.01f)] + public Vector2 StepSizeWhenStanding + { + get; + set; + } + /// /// In degrees. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 7da09bc6b..7a8544a75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -20,31 +20,31 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes), Editable] public Identifier SpeciesName { get; private set; } - [Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "References to another species. Define only if the creature is a variant that needs to use a pre-existing translation."), Editable] public Identifier SpeciesTranslationOverride { get; private set; } - [Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "Overrides the name of the character, shown to the player. If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] public string DisplayName { get; private set; } - [Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group consider each other friendly and do not attack each other."), Editable] public Identifier Group { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character is a humanoid and has different animation constraints relative to non-humanoid characters."), Editable(ReadOnly = true)] public bool Humanoid { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, jobs can be assigned to characters of this species. Should be true for the player characters."), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable] public bool CanInteract { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should this character be treated as a husk?"), Editable] public bool Husk { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"Should this character use a special husk appendage, attached to the ragdoll, when it turns into a husk?"), Editable] public bool UseHuskAppendage { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Does this character need oxygen to survive? Enabling this also makes the character vulnerable to high pressure when swimming outside of the submarine."), Editable] public bool NeedsAir { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] @@ -56,13 +56,13 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable] public bool IsMachine { get; set; } - [Serialize(false, IsPropertySaveable.No), Editable] + [Serialize(false, IsPropertySaveable.No, description:"Is the character able to send messages in the chat?"), Editable] public bool CanSpeak { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description:"Is there a health bar shown above the character when it takes damage? Defaults to true."), Editable] public bool ShowHealthBar { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Is this character's health shown at the top of the player's screen when they are in an active encounter?"), Editable] public bool UseBossHealthBar { get; private set; } [Serialize(100f, IsPropertySaveable.Yes, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] @@ -80,7 +80,7 @@ namespace Barotrauma [Serialize("waterblood", IsPropertySaveable.Yes), Editable] public string BleedParticleWater { get; private set; } - [Serialize(1f, IsPropertySaveable.Yes), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "A multiplier to increase or decrease the number of bleeding particles to create."), Editable] public float BleedParticleMultiplier { get; private set; } [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable] @@ -89,22 +89,22 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } - [Serialize(true, IsPropertySaveable.Yes), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the character AI use waypoints defined in the level to find a path to its targets?"), Editable] public bool UsePathFinding { get; set; } [Serialize(1f, IsPropertySaveable.Yes, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] public float PathFinderPriority { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden in the sonar?"), Editable] public bool HideInSonar { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden when using thermal goggles?"), Editable] public bool HideInThermalGoggles { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "If set to a value greater than zero, this character creates disrupting noise on the sonar when within range."), Editable] public float SonarDisruption { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "Range at which \"long distance\" blips for this character will appear on the sonar (used on some of the Abyss monsters)."), Editable] public float DistantSonarRange { get; set; } [Serialize(25000f, IsPropertySaveable.Yes, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] @@ -113,7 +113,7 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character be drawn on top of characters that do not have this set? This currently has no effect if the character has no deformable sprites."), Editable] public bool DrawLast { get; set; } [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable] @@ -543,7 +543,7 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool PoisonImmunity { get; set; } - + [Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)] public float PoisonVulnerability { get; set; } @@ -552,6 +552,19 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } + + [Serialize("", IsPropertySaveable.Yes, description:"A comma-separated list of identifiers of afflictions that the creature is immune to."), Editable] + public string Immunities { get; private set; } + + private ImmutableHashSet _immunityIdentifiers; + public IEnumerable ImmunityIdentifiers + { + get + { + _immunityIdentifiers ??= Element.GetAttributeIdentifierArray("immunities", Array.Empty()).ToImmutableHashSet(); + return _immunityIdentifiers; + } + } // TODO: limbhealths, sprite? @@ -634,6 +647,9 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] public float Hearing { get; private set; } + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Hard limit to how far the character can spot targets from, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable] + public float MaxPerceptionDistance { get; set; } + [Serialize(100f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)] public float AggressionHurt { get; private set; } @@ -673,7 +689,7 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"Unlike human AI, monsters normally only use pathfinding when they are inside the submarine. When this is enabled, the monsters can also use pathfinding to get inside the sub. In practice, via doors and hatches."), Editable] public bool UsePathFindingToGetInside { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] @@ -690,17 +706,18 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] public bool PatrolDry { get; set; } - - [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] + + [Serialize(0f, IsPropertySaveable.Yes, description: "Initial aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable] public float StartAggression { get; private set; } - [Serialize(100f, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(100f, IsPropertySaveable.Yes, description: "Maximum aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable] public float MaxAggression { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "How quickly the aggression level increases from StartAggression to MaxAggression when using the circle attack pattern. Artificial amount, applied once per attack cycle."), Editable] + public float AggressionCumulation { get; private set; } - [Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: ""), Editable] + [Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: "Defines the method of checking whether there's a blocking (submarine) wall."), Editable] public WallTargetingMethod WallTargetingMethod { get; private set; } public IEnumerable Targets => targets; @@ -851,6 +868,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] public bool IgnoreOutside { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside. Doesn't matter where the creature itself is."), Editable] + public bool IgnoreTargetInside { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's outside. Doesn't matter where the creature itself is."), Editable] + public bool IgnoreTargetOutside { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] public bool IgnoreIfNotInSameSub { get; set; } @@ -866,10 +889,16 @@ namespace Barotrauma [Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable] public float ThresholdMax { get; private set; } - [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further than it normally can."), Editable] + public float PerceptionDistanceMultiplier { get; private set; } + + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum distance at which the monster can perceive the target, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable] + public float MaxPerceptionDistance { get; private set; } + + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "A generic offset. Used for example for offsetting the react distance (vector length) and for offsetting the target position when a guardian flees to a pod."), Editable] public Vector2 Offset { get; private set; } - [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable] + [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes, description: "Defines the movement pattern of the character when approaching a target."), Editable] public AttackPattern AttackPattern { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable] @@ -887,31 +916,48 @@ namespace Barotrauma #endregion #region Circle - [Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] + [Serialize(5000f, IsPropertySaveable.Yes, description:"How close to the target the character should be, before they start using the circle pattern instead of directional approaching."), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] public float CircleStartDistance { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")] public bool IgnoreTargetSize { get; private set; } - [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)] + [Serialize(1f, IsPropertySaveable.Yes, description:"Determines the rate how quickly the target movement position is rotated towards the attack target. The actual rotation is calculated once per each attack cycle, based on the current aggression level."), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)] public float CircleRotationSpeed { get; private set; } [Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable] public bool DynamicCircleRotationSpeed { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)] + [Serialize(0f, IsPropertySaveable.Yes, description:"How much the turn speed can differ between attack cycles (stays constant during the cycle)"), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)] public float CircleRandomRotationFactor { get; private set; } - [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)] + [Serialize(5f, IsPropertySaveable.Yes, description:"Affects how close to the target the character has to be before the strike phase of the circle behavior triggers. In the strike phase, the creature moves directly towards the target."), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)] public float CircleStrikeDistanceMultiplier { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] + [Serialize(0f, IsPropertySaveable.Yes, description:"How much the target position is offset at maximum. Low values make the character hit the target earlier/always, higher values make it miss the target when the aggression intensity is low (early in the encounter)."), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] public float CircleMaxRandomOffset { get; private set; } #endregion - public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) { } + /// + /// Conditionals that must be met for the character to be able to use these targeting parameters. + /// + public List Conditionals { get; private set; } = new List(); - public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(character, tag, state, priority), character) { } + public TargetParams(string tag, AIState state, float priority, CharacterParams character) : + this(CreateNewElement(character, tag, state, priority), character) { } + + public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) + { + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "conditional": + Conditionals.AddRange(PropertyConditional.FromXElement(subElement)); + break; + } + } + } public static ContentXElement CreateNewElement(CharacterParams character, Identifier tag, AIState state, float priority) => CreateNewElement(character, tag.Value, state, priority); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 72ad781a8..dca3b5dc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -16,6 +16,7 @@ namespace Barotrauma public bool IsLoaded { get; protected set; } public string Name { get; private set; } public string FileName { get; private set; } + public string FileNameWithoutExtension { get; private set; } public string Folder { get; private set; } public ContentPath Path { get; protected set; } = ContentPath.Empty; public Dictionary SerializableProperties { get; protected set; } @@ -103,8 +104,9 @@ namespace Barotrauma { Path = fullPath; Name = GetName(); - FileName = System.IO.Path.GetFileName(Path.Value); - Folder = System.IO.Path.GetDirectoryName(Path.Value); + FileName = Barotrauma.IO.Path.GetFileName(Path.Value); + FileNameWithoutExtension = Barotrauma.IO.Path.GetFileNameWithoutExtension(Path.Value); + Folder = Barotrauma.IO.Path.GetDirectoryName(Path.Value); } public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 26e8ccfec..f4bf452ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Xml.Linq; using System.Linq; using Barotrauma.IO; @@ -13,15 +14,31 @@ using Barotrauma.SpriteDeformations; namespace Barotrauma { + public enum CanEnterSubmarine + { + /// + /// No part of the ragdoll can go inside a submarine + /// + False, + /// + /// Can fully enter a submarine. Make sure to only allow this on small/medium sized creatures that can reasonably fit inside rooms. + /// + True, + /// + /// The ragdoll's limbs can enter the sub, but the collider can't. + /// Can be used to e.g. allow the monster's head to poke into the sub to bite characters, even if the whole monster can't fit in the sub. + /// + Partial + } + class HumanRagdollParams : RagdollParams { - public static HumanRagdollParams GetRagdollParams(Identifier speciesName, string fileName = null) => GetRagdollParams(speciesName, fileName); - public static HumanRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); + public static HumanRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams(character); } class FishRagdollParams : RagdollParams { - public static FishRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); + public static FishRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams(character); } class RagdollParams : EditableParams, IMemorizable @@ -37,8 +54,11 @@ namespace Barotrauma [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } - - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "General orientation of the sprites as drawn on the spritesheet. " + + "Defines the \"forward direction\" of the sprites. Should be configured as the direction pointing outwards from the main limb. " + + "Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " + + "Can be overridden per sprite by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] public float SpritesheetOrientation { get; set; } public bool IsSpritesheetOrientationHorizontal @@ -53,11 +73,19 @@ namespace Barotrauma private float limbScale; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] - public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } + public float LimbScale + { + get { return limbScale; } + set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } + } private float jointScale; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] - public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } + public float JointScale + { + get { return jointScale; } + set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } + } // Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize? [Serialize(1f, IsPropertySaveable.No)] @@ -69,8 +97,8 @@ namespace Barotrauma [Serialize(50f, IsPropertySaveable.Yes, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float ImpactTolerance { get; set; } - [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] - public bool CanEnterSubmarine { get; set; } + [Serialize(CanEnterSubmarine.True, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] + public CanEnterSubmarine CanEnterSubmarine { get; set; } [Serialize(true, IsPropertySaveable.Yes), Editable] public bool CanWalk { get; set; } @@ -86,7 +114,7 @@ namespace Barotrauma /// key2: File path /// value: Ragdoll parameters /// - private readonly static Dictionary> allRagdolls = new Dictionary>(); + private static readonly Dictionary> allRagdolls = new Dictionary>(); public List Colliders { get; private set; } = new List(); public List Limbs { get; private set; } = new List(); @@ -106,8 +134,7 @@ namespace Barotrauma CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage)); if (prefab?.ConfigElement == null) { - DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", - contentPackage: contentPackage); + DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: contentPackage); return string.Empty; } return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value); @@ -115,99 +142,151 @@ namespace Barotrauma private static string GetFolder(ContentXElement root, string filePath) { - var folder = root?.GetChildElement("ragdolls")?.GetAttributeContentPath("folder")?.Value; + Debug.Assert(filePath != null); + Debug.Assert(root != null); + string folder = (root.GetChildElement("ragdolls") ?? root.GetChildElement("ragdoll"))?.GetAttributeContentPath("folder")?.Value; if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Ragdolls") + IO.Path.DirectorySeparatorChar; } return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } - - public static T GetDefaultRagdollParams(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName); - - /// - /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! - /// If a custom folder is used, it's defined in the character info file. - /// - public static T GetRagdollParams(Identifier speciesName, string fileName = null) where T : RagdollParams, new() + + public static T GetDefaultRagdollParams(Character character) where T : RagdollParams, new() => GetDefaultRagdollParams(character.SpeciesName, character.Params, character.Prefab.ContentPackage); + + public static T GetDefaultRagdollParams(Identifier speciesName, CharacterParams characterParams, ContentPackage contentPackage) where T : RagdollParams, new() { - if (speciesName.IsEmpty) + XElement mainElement = characterParams.VariantFile?.Root ?? characterParams.MainElement; + return GetDefaultRagdollParams(speciesName, mainElement, contentPackage); + } + + public static T GetDefaultRagdollParams(Identifier speciesName, XElement characterRootElement, ContentPackage contentPackage) where T : RagdollParams, new() + { + Debug.Assert(contentPackage != null); + if (characterRootElement.IsOverride()) { - throw new Exception($"Species name null or empty!"); + characterRootElement = characterRootElement.FirstElement(); } + Identifier ragdollSpecies = speciesName; + Identifier variantOf = characterRootElement.VariantOf(); + if (characterRootElement != null && (characterRootElement.GetChildElement("ragdolls") ?? characterRootElement.GetChildElement("ragdoll")) is XElement ragdollElement) + { + if ((ragdollElement.GetAttributeContentPath("path", contentPackage) ?? ragdollElement.GetAttributeContentPath("file", contentPackage)) is ContentPath path) + { + return GetRagdollParams(speciesName, ragdollSpecies, file: path, contentPackage); + } + else if (!variantOf.IsEmpty) + { + string folder = ragdollElement.GetAttributeContentPath("folder", contentPackage)?.Value; + if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + // Folder attribute not defined or set to default -> use the ragdoll defined in the base definition file. + if (CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab) + { + ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf); + } + } + } + } + else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab) + { + // Ragdoll element not defined -> use the ragdoll defined in the base definition file. + ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf); + } + // Using a null file definition means we use the default animations found in the Ragdolls folder. + return GetRagdollParams(speciesName, ragdollSpecies, file: null, contentPackage); + } + + public static T GetRagdollParams(Identifier speciesName, Identifier ragdollSpecies, Either file, ContentPackage contentPackage) where T : RagdollParams, new() + { + Debug.Assert(!speciesName.IsEmpty); + Debug.Assert(!ragdollSpecies.IsEmpty); + ContentPath contentPath = null; + string fileName = null; + if (file != null) + { + if (!file.TryGet(out fileName)) + { + file.TryGet(out contentPath); + } + Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace()); + } + Debug.Assert(contentPackage != null); if (!allRagdolls.TryGetValue(speciesName, out Dictionary ragdolls)) { ragdolls = new Dictionary(); allRagdolls.Add(speciesName, ragdolls); } - if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) + string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies); + if (ragdolls.TryGetValue(key, out RagdollParams ragdoll)) { + // Already cached. return (T)ragdoll; } - string selectedFile = null; - Identifier ragdollSpecies = speciesName; - if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)) + if (!contentPath.IsNullOrEmpty()) { - if (!prefab.VariantOf.IsEmpty) + // Load the ragdoll from path. + T ragdollInstance = new T(); + if (ragdollInstance.Load(contentPath, ragdollSpecies)) { - ragdollSpecies = prefab.VariantOf; + ragdolls.TryAdd(contentPath.Value, ragdollInstance); + return ragdollInstance; } - string error = null; - string folder = GetFolder(ragdollSpecies); - if (!Directory.Exists(folder)) + else { - error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."; + DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage); + } + } + // Seek the default ragdoll from the character's ragdoll folder. + string selectedFile; + string folder = GetFolder(ragdollSpecies); + if (Directory.Exists(folder)) + { + var files = Directory.GetFiles(folder).OrderBy(f => f, StringComparer.OrdinalIgnoreCase); + if (files.None()) + { + DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.", contentPackage: contentPackage); selectedFile = GetDefaultFile(ragdollSpecies); } else { - string[] files = Directory.GetFiles(folder); - if (files.None()) + if (string.IsNullOrEmpty(fileName)) { - error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."; - selectedFile = GetDefaultFile(ragdollSpecies); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified - selectedFile = GetDefaultFile(ragdollSpecies); + // Files found, but none specified -> Get a matching ragdoll from the specified folder. + // First try to find a file that matches the default file name. If that fails, just take any file. + string defaultFileName = GetDefaultFileName(ragdollSpecies); + selectedFile = files.FirstOrDefault(f => f.Contains(defaultFileName, StringComparison.OrdinalIgnoreCase)) ?? files.First(); } else { selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { - error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; + DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.", contentPackage: contentPackage); selectedFile = GetDefaultFile(ragdollSpecies); } - } + } } - if (error != null) - { - DebugConsole.ThrowError(error, - contentPackage: prefab?.ContentPackage); - } - } - if (selectedFile == null) - { - throw new Exception("[RagdollParams] Selected file null!"); - } - DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); - var characterPrefab = CharacterPrefab.Prefabs[speciesName]; - T r = new T(); - if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies)) - { - if (!ragdolls.ContainsKey(r.Name)) - { - ragdolls.Add(r.Name, r); - } - return r; } else { - // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early. + DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.", contentPackage: contentPackage); + selectedFile = GetDefaultFile(ragdollSpecies); + } + + Debug.Assert(selectedFile != null); + DebugConsole.Log($"[RagdollParams] Loading the ragdoll from {selectedFile}."); + T r = new T(); + if (r.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName)) + { + ragdolls.TryAdd(key, r); + } + else + { + // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harder to debug. It's better to fail early. throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."); } + return r; } /// @@ -234,9 +313,9 @@ namespace Barotrauma instance.IsLoaded = instance.Deserialize(mainElement); instance.Save(); instance.Load(contentPath, speciesName); - ragdolls.Add(instance.Name, instance); + ragdolls.Add(instance.FileNameWithoutExtension, instance); DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite); - return instance as T; + return instance; } public static void ClearCache() => allRagdolls.Clear(); @@ -250,16 +329,17 @@ namespace Barotrauma else { // Update the key by removing and re-adding the ragdoll. + string fileName = FileNameWithoutExtension; if (allRagdolls.TryGetValue(SpeciesName, out Dictionary ragdolls)) { - ragdolls.Remove(Name); + ragdolls.Remove(fileName); } base.UpdatePath(fullPath); if (ragdolls != null) { - if (!ragdolls.ContainsKey(Name)) + if (!ragdolls.ContainsKey(fileName)) { - ragdolls.Add(Name, this); + ragdolls.Add(fileName, this); } } } @@ -282,6 +362,7 @@ namespace Barotrauma { if (Load(file)) { + isVariantScaleApplied = false; SpeciesName = speciesName; CreateColliders(); CreateLimbs(); @@ -398,18 +479,21 @@ namespace Barotrauma } #endif - private bool variantScaleApplied; - public void ApplyVariantScale(XDocument variantFile) + private bool isVariantScaleApplied; + public void TryApplyVariantScale(XDocument variantFile) { - if (variantScaleApplied) { return; } + if (isVariantScaleApplied) { return; } if (variantFile == null) { return; } - var scaleMultiplier = variantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("scalemultiplier", 1f); - if (scaleMultiplier.HasValue) + if (variantFile.GetRootExcludingOverride() is XElement root) { - JointScale *= scaleMultiplier.Value; - LimbScale *= scaleMultiplier.Value; + if ((root.GetChildElement("ragdoll") ?? root.GetChildElement("ragdolls")) is XElement ragdollElement) + { + float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f); + JointScale *= scaleMultiplier; + LimbScale *= scaleMultiplier; + } } - variantScaleApplied = true; + isVariantScaleApplied = true; } #endregion @@ -623,8 +707,11 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - - [Serialize(float.NaN, IsPropertySaveable.Yes, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] + + [Serialize(float.NaN, IsPropertySaveable.Yes, description: "Orientation of the sprite as drawn on the spritesheet. " + + "Defines the \"forward direction\" of the sprite. Should be configured as the direction pointing outwards from the main limb." + + "Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " + + "Overrides the value of 'Spritesheet Orientation' for this limb."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] public float SpriteOrientation { get; set; } [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")] @@ -744,6 +831,9 @@ namespace Barotrauma [Serialize(0.05f, IsPropertySaveable.Yes)] public float Restitution { get; set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Can the limb enter submarines? Only valid if the ragdoll's CanEnterSubmarine is set to Partial, otherwise the limb can enter if the ragdoll can."), Editable] + public bool CanEnterSubmarine { get; private set; } + public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { var spriteElement = element.GetChildElement("sprite"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs index 961cfcf71..ac37854de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -12,9 +12,9 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + if (abilityObject is IAbilityAffliction { Affliction: Affliction affliction }) { - return afflictions.Any(a => a == affliction.Identifier); + return afflictions.Any(a => a == affliction.Identifier || a == affliction.Prefab.AfflictionType); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionInSubmarine.cs similarity index 57% rename from Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs rename to Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionInSubmarine.cs index f237deea3..a4bbb17f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionInSubmarine.cs @@ -1,16 +1,15 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { - class AbilityConditionItemInSubmarine : AbilityConditionData + [TypePreviouslyKnownAs("AbilityConditionItemInSubmarine")] + class AbilityConditionInSubmarine : AbilityConditionData { private readonly SubmarineType? submarineType; - public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { if (conditionElement.GetAttribute("submarinetype") != null) { - submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); + submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); } } @@ -30,9 +29,15 @@ namespace Barotrauma.Abilities } else { - LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); - return false; + return MatchesCondition(); } } + + public override bool MatchesCondition() + { + if (character.Submarine is null) { return false; } + + return character.Submarine?.Info?.Type == submarineType; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs index f14552583..512ef0c01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs @@ -8,6 +8,7 @@ namespace Barotrauma.Abilities { private readonly Option matchedLevel; private readonly Option minLevel; + private readonly Option maxLevel; public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { @@ -18,23 +19,33 @@ namespace Barotrauma.Abilities minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0 ? Option.Some(min) : Option.None(); + + maxLevel = conditionElement.GetAttributeInt("maxlevel", 0) is var max and not 0 + ? Option.Some(max) + : Option.None(); - if (matchedLevel.IsNone() && minLevel.IsNone()) + if (matchedLevel.IsNone() && minLevel.IsNone() && maxLevel.IsNone()) { - throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute."); + throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\", \"minlevel\" or \"maxlevel\" attribute."); } } protected override bool MatchesConditionSpecific() { + var currentLevel = character.Info.GetCurrentLevel(); if (matchedLevel.TryUnwrap(out int match)) { - return character.Info.GetCurrentLevel() == match; + return currentLevel == match; } if (minLevel.TryUnwrap(out int min)) { - return character.Info.GetCurrentLevel() >= min; + return currentLevel >= min; + } + + if (maxLevel.TryUnwrap(out int max)) + { + return currentLevel <= max; } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs index 3cc7c0eac..231b01487 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs @@ -12,7 +12,9 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific() { - return character.IsRagdolled || character.Stun > 0f || character.IsIncapacitated; + // TODO: Should we only check whether the target is ragdolling here? + // Or should we use character.IsKnockedDown instead? + return (character.IsRagdolled && !character.AnimController.IsHangingWithRope) || character.Stun > 0f || character.IsIncapacitated; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 7ccc4e036..9d2ab1c13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Abilities string type = abilityElement.Name.ToString().ToLowerInvariant(); try { - abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + abilityType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true); if (abilityType == null) { if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 3b9653393..1eba6a358 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -21,24 +21,42 @@ } } + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect() + { + ApplyAfflictionToCharacter(Character); + } + protected override void ApplyEffect(AbilityObject abilityObject) { if (abilityObject is IAbilityCharacter character) { - var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId); - if (afflictionPrefab == null) - { - DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".", - contentPackage: CharacterTalent.Prefab.ContentPackage); - return; - } - float strength = this.strength; - if (!string.IsNullOrEmpty(multiplyStrengthBySkill)) - { - strength *= Character.GetSkillLevel(multiplyStrengthBySkill); - } - character.Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue); + ApplyAfflictionToCharacter(character.Character); } } + + private void ApplyAfflictionToCharacter(Character character) + { + var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId); + if (afflictionPrefab == null) + { + DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".", + contentPackage: CharacterTalent.Prefab.ContentPackage); + return; + } + float strength = this.strength; + if (!string.IsNullOrEmpty(multiplyStrengthBySkill)) + { + strength *= Character.GetSkillLevel(multiplyStrengthBySkill); + } + character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 062032ca7..98269e784 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -15,12 +15,13 @@ DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.", contentPackage: abilityElement.ContentPackage); } + + // NOTE: The resistance value is a multiplier here, so 1.0 == 0% resistance if (MathUtils.NearlyEqual(multiplier, 1)) { DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.", contentPackage: abilityElement.ContentPackage); } - } public override void InitializeAbility(bool addingFirstTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs index 4f64d8bfd..c519be61a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(AbilityObject abilityObject) { if (abilityObject is not IAbilityCharacter character) { return; } - character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount); + character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount, attacker: Character); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs new file mode 100644 index 000000000..a7387e414 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUpgradeSubmarine.cs @@ -0,0 +1,57 @@ +#nullable enable +namespace Barotrauma.Abilities; + +internal class CharacterAbilityUpgradeSubmarine : CharacterAbility +{ + private readonly UpgradePrefab? upgradePrefab; + private readonly UpgradeCategory? upgradeCategory; + public readonly int level; + + public override bool AllowClientSimulation => true; + + public CharacterAbilityUpgradeSubmarine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + var prefabIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradePrefab), Identifier.Empty); + var categoryIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradeCategory), Identifier.Empty); + + if (UpgradePrefab.Find(prefabIdentifier) is not { } foundUpgradePrefab) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradePrefab)} not found.", + contentPackage: abilityElement.ContentPackage); + } + else + { + upgradePrefab = foundUpgradePrefab; + } + + if (UpgradeCategory.Find(categoryIdentifier) is not { } foundUpgradeCategory) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradeCategory)} not found.", + contentPackage: abilityElement.ContentPackage); + } + else + { + upgradeCategory = foundUpgradeCategory; + } + + level = abilityElement.GetAttributeInt(nameof(level), 1); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + private void ApplyEffectSpecific() + { + if (upgradePrefab == null || upgradeCategory == null) { return; } + if (GameMain.GameSession?.Campaign?.UpgradeManager is not { } upgradeManager) { return; } + + upgradeManager.AddUpgradeExternally(upgradePrefab, upgradeCategory, level); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs new file mode 100644 index 000000000..7348428ae --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityWarStories.cs @@ -0,0 +1,63 @@ +namespace Barotrauma.Abilities; + +/// +/// Hardcoded ability for the "War Stories" talent. +/// Spawns an item and sets the health multiplier to the target stat value. +/// +/// The item spawned should have a default health of 1 because we set the multiplier. +/// This is because we already had existing Item.HealthMultiplier that gets synced and +/// everything but not one for setting the max health directly to some value and I didn't +/// want to add a new one just for this. +/// +internal class CharacterAbilityWarStories : CharacterAbility +{ + private readonly Identifier targetStat; + private readonly float minCondition; + + private readonly ItemPrefab prefab; + + public CharacterAbilityWarStories(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + targetStat = abilityElement.GetAttributeIdentifier("target", Identifier.Empty); + minCondition = abilityElement.GetAttributeFloat("mincondition", 1); + + if (targetStat.IsEmpty) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityWarStories)}: target stat is not defined", contentPackage: abilityElement.ContentPackage); + } + + Identifier spawnedItem = abilityElement.GetAttributeIdentifier("item", Identifier.Empty); + if (!ItemPrefab.Prefabs.TryGet(spawnedItem, out prefab)) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityWarStories)}: spawned item \"{spawnedItem}\" could not be found.", contentPackage: abilityElement.ContentPackage); + } + } + + protected override void ApplyEffect() + { + if (prefab is null || Character is null) { return; } + + float condition = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0; + if (condition < minCondition) { return; } + + if (GameMain.GameSession?.RoundEnding ?? true) + { + Item item = new(prefab, Character.WorldPosition, Character.Submarine) + { + Condition = condition, + HealthMultiplier = condition + }; + Character.Inventory.TryPutItem(item, Character, item.AllowedSlots); + } + else + { + Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, condition: condition, onSpawned: item => + { + item.HealthMultiplier = condition; + }); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + => ApplyEffect(); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 4d177dd69..aa8b70120 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Abilities string type = conditionElement.Name.ToString().ToLowerInvariant(); try { - conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + conditionType = ReflectionUtils.GetTypeWithBackwardsCompatibility("Barotrauma.Abilities", type, false, true); if (conditionType == null) { if (errorMessages) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index f640dee46..736421c36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -22,6 +22,12 @@ namespace Barotrauma public readonly Sprite Icon; + /// + /// When set to a value the talent tooltip will display a text showing the current value of the stat and the max value. + /// For example "Progress: 37/100". + /// + public readonly Option<(Identifier PermanentStatIdentifier, int Max)> TrackedStat; + #if CLIENT public readonly Option ColorOverride; #endif @@ -44,6 +50,12 @@ namespace Barotrauma AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true); + var trackedStat = element.GetAttributeIdentifier("trackedstat", Identifier.Empty); + var trackedMax = element.GetAttributeInt("trackedmax", 100); + TrackedStat = !trackedStat.IsEmpty + ? Option.Some((trackedStat, trackedMax)) + : Option.None; + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); if (!nameIdentifier.IsEmpty) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 04ac83453..bb06f8717 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -82,18 +82,17 @@ namespace Barotrauma TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); if (subTree is null) { return TalentStages.Invalid; } - if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) - { - return TalentStages.Locked; - } - TalentOption targetTalentOption = subTree.TalentOptionStages[index]; - if (targetTalentOption.HasEnoughTalents(character.Info)) { return TalentStages.Unlocked; } + if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) + { + return TalentStages.Locked; + } + if (targetTalentOption.HasSelectedTalent(selectedTalents)) { return TalentStages.Highlighted; diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxLabelNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxLabelNode.cs new file mode 100644 index 000000000..058018ceb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxLabelNode.cs @@ -0,0 +1,83 @@ +#nullable enable + +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal sealed partial class CircuitBoxLabelNode : CircuitBoxNode, ICircuitBoxIdentifiable + { + public Color Color; + public ushort ID { get; } + + public override bool IsResizable => true; + + public static NetLimitedString DefaultHeaderText => new("label"); + + public NetLimitedString BodyText = NetLimitedString.Empty; + public NetLimitedString HeaderText = DefaultHeaderText; + + public static Vector2 MinSize = new(128, 8); + + public CircuitBoxLabelNode(ushort id, Color color, Vector2 pos, CircuitBox circuitBox) : base(circuitBox) + { + Size = new Vector2(256); + Position = pos; + ID = id; + Color = color; + UpdatePositions(); +#if CLIENT + bodyLabel = new GUITextBlock(new RectTransform(Point.Zero), text: string.Empty, font: GUIStyle.Font, textAlignment: Alignment.TopLeft, wrap: true); + headerLabel = new CircuitBoxLabel(HeaderText.Value, GUIStyle.LargeFont); + UpdateDrawRects(); + UpdateTextSizes(DrawRect); +#endif + } + + public void EditText(NetLimitedString header, NetLimitedString body) + { + HeaderText = header; + BodyText = body; +#if CLIENT + UpdateTextSizes(DrawRect); +#endif + } + + public XElement Save() + { + var element = new XElement("Label", + new XAttribute("id", ID), + new XAttribute("color", Color.ToStringHex()), + new XAttribute("position", XMLExtensions.Vector2ToString(Position)), + new XAttribute("size", XMLExtensions.Vector2ToString(Size)), + new XAttribute("header", HeaderText), + new XAttribute("body", BodyText)); + return element; + } + + public static CircuitBoxLabelNode LoadFromXML(ContentXElement element, CircuitBox circuitBox) + { + ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID); + Vector2 position = element.GetAttributeVector2("position", Vector2.Zero); + Vector2 size = element.GetAttributeVector2("size", Vector2.Zero); + Color color = element.GetAttributeColor("color", Color.White); + string header = element.GetAttributeString("header", string.Empty); + string body = element.GetAttributeString("body", string.Empty); + + var labelNode = new CircuitBoxLabelNode(id, color, position, circuitBox) + { + Size = size, + HeaderText = new NetLimitedString(header), + BodyText = new NetLimitedString(body) + }; + // proc a edit to force the sizes to be updated + labelNode.EditText(new NetLimitedString(header), new NetLimitedString(body)); + labelNode.UpdatePositions(); +#if CLIENT + labelNode.UpdateTextSizes(labelNode.Rect); +#endif + return labelNode; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs index cab96ebee..2d7ad4f36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNetStructs.cs @@ -8,6 +8,19 @@ using Microsoft.Xna.Framework; namespace Barotrauma { + [Flags] + internal enum CircuitBoxResizeDirection + { + None = 0, + Down = 1, + Right = 2, + Left = 4 + } + + // TODO this needs to be refactored at some point for reasons: + // 1. We need to send 4 different ImmutableArray for some network packets + // 2. We have 3 identical remove events that are identical in signature + // 3. We have 3 different events for selecting. nodes, wires, and server broadcast public enum CircuitBoxOpcode { Error, @@ -20,6 +33,10 @@ namespace Barotrauma SelectWires, UpdateSelection, DeleteComponent, + RenameLabel, + AddLabel, + RemoveLabel, + ResizeLabel, ServerInitialize } @@ -88,6 +105,18 @@ namespace Barotrauma => $"{{Name: {SignalConnection}, ID: {(TargetId.TryUnwrap(out var value) ? value.ToString() : "N/A")}}}"; } + [NetworkSerialize] + internal readonly record struct CircuitBoxAddLabelEvent(Vector2 Position, Color Color, NetLimitedString Header, NetLimitedString Body) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxServerAddLabelEvent(ushort ID, Vector2 Position, Vector2 Size, Color Color, NetLimitedString Header, NetLimitedString Body) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxResizeLabelEvent(ushort ID, Vector2 Position, Vector2 Size) : INetSerializableStruct; + + [NetworkSerialize] + internal readonly record struct CircuitBoxRemoveLabelEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + [NetworkSerialize] internal readonly record struct CircuitBoxAddComponentEvent(UInt32 PrefabIdentifier, Vector2 Position) : INetSerializableStruct; @@ -98,13 +127,13 @@ namespace Barotrauma internal readonly record struct CircuitBoxRemoveComponentEvent(ImmutableArray TargetIDs) : INetSerializableStruct; [NetworkSerialize] - internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray TargetIDs, ImmutableArray IOs, Vector2 MoveAmount) : INetSerializableStruct; + internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray TargetIDs, ImmutableArray IOs, ImmutableArray LabelIDs, Vector2 MoveAmount) : INetSerializableStruct; [NetworkSerialize] - internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray TargetIDs, ImmutableArray IOs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; + internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray TargetIDs, ImmutableArray IOs, ImmutableArray LabelIDs, bool Overwrite, ushort CharacterID) : INetSerializableStruct; [NetworkSerialize] - internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray ComponentIds, ImmutableArray WireIds, ImmutableArray InputOutputs) : INetSerializableStruct; + internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray ComponentIds, ImmutableArray WireIds, ImmutableArray InputOutputs, ImmutableArray LabelIds) : INetSerializableStruct; [NetworkSerialize] internal readonly record struct CircuitBoxIdSelectionPair(ushort ID, Option SelectedBy) : INetSerializableStruct; @@ -124,6 +153,9 @@ namespace Barotrauma [NetworkSerialize] internal readonly record struct CircuitBoxRemoveWireEvent(ImmutableArray TargetIDs) : INetSerializableStruct; + [NetworkSerialize] + internal readonly record struct CircuitBoxRenameLabelEvent(ushort LabelId, Color Color, NetLimitedString NewHeader, NetLimitedString NewBody) : INetSerializableStruct; + [NetworkSerialize] internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct; @@ -131,6 +163,7 @@ namespace Barotrauma internal readonly record struct CircuitBoxInitializeStateFromServerEvent( ImmutableArray Components, ImmutableArray Wires, + ImmutableArray Labels, Vector2 InputPos, Vector2 OutputPos) : INetSerializableStruct; @@ -157,6 +190,14 @@ namespace Barotrauma => CircuitBoxOpcode.RemoveWire, CircuitBoxInitializeStateFromServerEvent => CircuitBoxOpcode.ServerInitialize, + CircuitBoxRenameLabelEvent + => CircuitBoxOpcode.RenameLabel, + (CircuitBoxAddLabelEvent or CircuitBoxServerAddLabelEvent) + => CircuitBoxOpcode.AddLabel, + CircuitBoxRemoveLabelEvent + => CircuitBoxOpcode.RemoveLabel, + CircuitBoxResizeLabelEvent + => CircuitBoxOpcode.ResizeLabel, _ => throw new ArgumentOutOfRangeException(nameof(Data)) }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs index 0dbb018f8..3608618ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxNode.cs @@ -14,6 +14,8 @@ namespace Barotrauma public RectangleF Rect; private Vector2 position; + public virtual bool IsResizable => false; + public Vector2 Position { get => position; @@ -22,12 +24,12 @@ namespace Barotrauma const float clampSize = CircuitBoxSizes.PlayableAreaSize / 2f; position = new Vector2(Math.Clamp(value.X, -clampSize, clampSize), - Math.Clamp(value.Y, -clampSize, clampSize)); + Math.Clamp(value.Y, -clampSize, clampSize)); UpdatePositions(); } } - public ImmutableArray Connectors; + public ImmutableArray Connectors = ImmutableArray.Empty; public static float Opacity = 0.8f; @@ -38,6 +40,47 @@ namespace Barotrauma CircuitBox = circuitBox; } + public (Vector2 Size, Vector2 Pos) ResizeBy(CircuitBoxResizeDirection directions, Vector2 amount) + { + Vector2 newSize = Size; + Vector2 newPos = Position; + amount.Y = -amount.Y; + + if (directions.HasFlag(CircuitBoxResizeDirection.Down)) + { + newSize.Y += amount.Y; + newSize.Y = Math.Max(newSize.Y, CircuitBoxLabelNode.MinSize.Y); + newPos = new Vector2(newPos.X, newPos.Y - (newSize.Y - Size.Y) / 2f); + } + + if (directions.HasFlag(CircuitBoxResizeDirection.Right)) + { + newSize.X += amount.X; + newSize.X = Math.Max(newSize.X, CircuitBoxLabelNode.MinSize.X); + newPos = new Vector2(newPos.X + (newSize.X - Size.X) / 2f, newPos.Y); + } + + if (directions.HasFlag(CircuitBoxResizeDirection.Left)) + { + newSize.X -= amount.X; + newSize.X = Math.Max(newSize.X, CircuitBoxLabelNode.MinSize.X); + newPos = new Vector2(newPos.X + (Size.X - newSize.X) / 2f, newPos.Y); + } + + return (newSize, newPos); + } + + public void ApplyResize(Vector2 newSize, Vector2 newPos) + { + if (!MathUtils.IsValid(newSize)) { return; } + Size = newSize; + Position = newPos; + UpdatePositions(); +#if CLIENT + OnResized(DrawRect); +#endif + } + public static Vector2 CalculateSize(IReadOnlyList conns) { Vector2 leftSize = Vector2.Zero, diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs index 5f7f211b8..b21649561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxSizes.cs @@ -11,6 +11,7 @@ namespace Barotrauma public const int WireWidth = 10; public const int WireKnobLength = 16; public const int NodeHeaderTextPadding = 8; + public const int NodeBodyTextPadding = 8; public const float PlayableAreaSize = 8192f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs index 843b873fb..ba2e8e480 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CircuitBox/CircuitBoxWire.cs @@ -148,8 +148,14 @@ namespace Barotrauma } case CircuitBoxNodeConnection node when two is CircuitBoxInputConnection input: { - if (node.ExternallyConnectedFrom.Contains(input)) { break; } - node.ExternallyConnectedFrom.Add(input); + if (!node.Connection.CircuitBoxConnections.Contains(input)) + { + node.Connection.CircuitBoxConnections.Add(input); + } + if (!node.ExternallyConnectedFrom.Contains(input)) + { + node.ExternallyConnectedFrom.Add(input); + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs index d8269dbd0..96dc4ca72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -14,6 +14,7 @@ namespace Barotrauma public override void LoadFile() { + ClearCaches(); XDocument doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { @@ -36,6 +37,15 @@ namespace Barotrauma public override void UnloadFile() { CharacterPrefab.Prefabs.RemoveByFile(this); + ClearCaches(); + } + + private static void ClearCaches() + { + // Clear the caches to get rid of any overrides. + // Variants should have their own params instances, but let's keep it simple and play safe. + RagdollParams.ClearCache(); + AnimationParams.ClearCache(); } public override void Sort() @@ -60,11 +70,11 @@ namespace Barotrauma { if (humanoid) { - ragdollParams = RagdollParams.GetRagdollParams(speciesName); + ragdollParams = RagdollParams.GetDefaultRagdollParams(speciesName, mainElement, ContentPackage); } else { - ragdollParams = RagdollParams.GetRagdollParams(speciesName); + ragdollParams = RagdollParams.GetDefaultRagdollParams(speciesName, mainElement, ContentPackage); } } catch (Exception e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContainerTagFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContainerTagFile.cs new file mode 100644 index 000000000..b121a2147 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContainerTagFile.cs @@ -0,0 +1,16 @@ +#nullable enable + +namespace Barotrauma +{ + internal sealed class ContainerTagFile : GenericPrefabFile + { + public ContainerTagFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "containertag"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "containertags"; + protected override PrefabCollection Prefabs => ContainerTagPrefab.Prefabs; + + protected override ContainerTagPrefab CreatePrefab(ContentXElement element) + => new(element, this); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs index ff4493c9c..978429cbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -37,7 +37,8 @@ namespace Barotrauma if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); } } TextManager.IncrementLanguageVersion(); - if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language)) + if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language) && + GameSettings.CurrentConfig.Language != TextManager.DefaultLanguage) { DebugConsole.AddWarning($"The language {GameSettings.CurrentConfig.Language} is no longer available. Switching to {TextManager.DefaultLanguage}..."); var config = GameSettings.CurrentConfig; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 398658520..b14e575b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -65,8 +65,13 @@ namespace Barotrauma public static void ReloadCore() { if (Core == null) { return; } - Core.UnloadContent(); - Core.LoadContent(); + ReloadPackage(Core); + } + + public static void ReloadPackage(ContentPackage p) + { + p.UnloadContent(); + p.LoadContent(); SortContent(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 149395221..0d9a4a112 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -22,7 +22,7 @@ namespace Barotrauma private string? cachedValue; private string? cachedFullPath; - + public string Value { get @@ -107,11 +107,6 @@ namespace Barotrauma prevCreatedRaw = newRaw; return newRaw; } - - public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue) - { - throw new NotImplementedException(); - } private static bool StringEquality(string? a, string? b) { @@ -119,8 +114,8 @@ namespace Barotrauma { return a.IsNullOrEmpty() == b.IsNullOrEmpty(); } - return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), - Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), StringComparison.OrdinalIgnoreCase); + return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(correctFilenameCase: false) ?? ""), + Path.GetFullPath(b.CleanUpPathCrossPlatform(correctFilenameCase: false) ?? ""), StringComparison.OrdinalIgnoreCase); } public static bool operator==(ContentPath a, ContentPath b) @@ -145,9 +140,9 @@ namespace Barotrauma public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (ReferenceEquals(null, obj)) { return false; } + if (ReferenceEquals(this, obj)) { return true; } + if (obj.GetType() != this.GetType()) { return false; } return Equals((ContentPath)obj); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 062bd0a4c..74f59fcd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -740,6 +740,35 @@ namespace Barotrauma }; }, isCheat: true)); + + commands.Add(new Command("listsuitabletreatments", "listsuitabletreatments [character name]: List which items are the most suitable for treating the specified character. Useful for debugging medic AI.", (string[] args) => + { + Character character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); + if (character != null) + { + Dictionary treatments = new Dictionary(); + character.CharacterHealth.GetSuitableTreatments(treatments, user: null); + foreach (var treatment in treatments.OrderByDescending(t => t.Value)) + { + Color color = Color.White; +#if CLIENT + color = ToolBox.GradientLerp( + MathUtils.InverseLerp(-1000, 1000, treatment.Value), + Color.Red, Color.Yellow, Color.White, Color.LightGreen); +#endif + NewMessage((int)treatment.Value + ": " + treatment.Key, color); + + } + } + }, + () => + { + return new string[][] + { + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() + }; + }, isCheat: true)); + commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) => { Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); @@ -828,7 +857,7 @@ namespace Barotrauma } else if (eventPrefab != null) { - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); if (newEvent == null) { NewMessage($"Could not initialize event {args[0]} because level did not meet requirements"); @@ -2297,6 +2326,7 @@ namespace Barotrauma #endif } spawnedCharacter.GiveJobItems(spawnPoint); + spawnedCharacter.GiveIdCardTags(spawnPoint); spawnedCharacter.Info.StartItemsGiven = true; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 9fcaed105..2f71320e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -152,6 +152,7 @@ namespace Barotrauma OnAllyGainMissionExperience, OnGainMissionExperience, OnGainMissionMoney, + OnCrewGainMissionReputation, OnLocationDiscovered, OnItemDeconstructed, OnItemDeconstructedByAlly, @@ -329,6 +330,11 @@ namespace Barotrauma /// Increases the repair speed of the character when repairing mechanical items by a percentage. /// MechanicalRepairSpeed, + + /// + /// Increases the repair speed of the character when repairing electrical items by a percentage. + /// + ElectricalRepairSpeed, /// /// Increase deconstruction speed of deconstructor operated by the character by a percentage. @@ -572,7 +578,12 @@ namespace Barotrauma /// /// Modifies how far the character can be seen from (can be used to make the character easier or more difficult for monsters to see) /// - SightRangeMultiplier + SightRangeMultiplier, + + /// + /// Reduces the dual wielding penalty by a percentage. + /// + DualWieldingPenaltyReduction } internal enum ItemTalentStats @@ -672,18 +683,26 @@ namespace Barotrauma Both = Bot | Player } - public enum StartingBalanceAmount + public enum StartingBalanceAmountOption { Low, Medium, High, } - public enum GameDifficulty + public enum PatdownProbabilityOption { - Easy, + Off, + Low, Medium, - Hard, + High, + } + + public enum WorldHostilityOption + { + Low, + Medium, + High, Hellish } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index da3967687..e178bd9f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -29,8 +29,8 @@ namespace Barotrauma return $"ArtifactEvent ({(itemPrefab == null ? "null" : itemPrefab.Name)})"; } - public ArtifactEvent(EventPrefab prefab) - : base(prefab) + public ArtifactEvent(EventPrefab prefab, int seed) + : base(prefab, seed) { if (prefab.ConfigElement.GetAttribute("itemname") != null) { @@ -55,9 +55,8 @@ namespace Barotrauma } } - public override void Init(EventSet parentSet) + protected override void InitEventSpecific(EventSet parentSet) { - base.Init(parentSet); spawnPos = Level.Loaded.GetRandomItemPos( (Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ? Level.PositionType.MainPath | Level.PositionType.SidePath : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 0e4358efb..08abda9ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -9,7 +9,7 @@ namespace Barotrauma public event Action Finished; protected bool isFinished; - public int RandomSeed; + public readonly int RandomSeed; protected readonly EventPrefab prefab; @@ -17,6 +17,8 @@ namespace Barotrauma public EventSet ParentSet { get; private set; } + public bool Initialized { get; private set; } + public Func SpawnPosFilter; public bool IsFinished @@ -37,8 +39,9 @@ namespace Barotrauma } } - public Event(EventPrefab prefab) + public Event(EventPrefab prefab, int seed) { + RandomSeed = seed; this.prefab = prefab ?? throw new ArgumentNullException(nameof(prefab)); } @@ -47,9 +50,15 @@ namespace Barotrauma yield break; } - public virtual void Init(EventSet parentSet = null) + public void Init(EventSet parentSet = null) { + Initialized = true; ParentSet = parentSet; + InitEventSpecific(parentSet); + } + + protected virtual void InitEventSpecific(EventSet parentSet = null) + { } public virtual string GetDebugInfo() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs index a5b62d869..58e25ea73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs @@ -1,22 +1,35 @@ -using System.Linq; +using System.Linq; namespace Barotrauma { + /// + /// Gives an affliction to a specific character. + /// class AfflictionAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the affliction.")] public Identifier Affliction { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Strength of the affliction.")] public float Strength { get; set; } - [Serialize(LimbType.None, IsPropertySaveable.Yes)] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "Type of the limb(s) to apply the affliction on. Only valid if the affliction is limb-specific.")] public LimbType LimbType { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to apply the affliction on.")] public Identifier TargetTag { get; set; } - public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the strength be multiplied by the maximum vitality of the target?")] + public bool MultiplyByMaxVitality { get; set; } + + public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (Affliction.IsEmpty) + { + DebugConsole.ThrowError($"Error in {nameof(AfflictionAction)}: affliction not defined (use the attribute \"{nameof(Affliction)}\").", + contentPackage: element.ContentPackage); + } + } private bool isFinished = false; @@ -40,27 +53,32 @@ namespace Barotrauma { if (target != null && target is Character character) { + float strength = Strength; + if (MultiplyByMaxVitality) + { + strength *= character.MaxVitality; + } if (LimbType != LimbType.None) { var limb = character.AnimController.GetLimb(LimbType); - if (Strength > 0.0f) + if (strength > 0.0f) { - character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true); + character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(strength), ignoreUnkillability: true); } - else if (Strength < 0.0f) + else if (strength < 0.0f) { - character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -Strength); + character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -strength); } } else { - if (Strength > 0.0f) + if (strength > 0.0f) { - character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true); + character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), ignoreUnkillability: true); } - else if (Strength < 0.0f) + else if (strength < 0.0f) { - character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -Strength); + character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -strength); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index fb75303ee..5977105bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -4,24 +4,27 @@ using System.Linq; namespace Barotrauma { + /// + /// Check whether a target has a specific affliction. + /// internal class CheckAfflictionAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the affliction.")] public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier TargetTag { get; set; } = Identifier.Empty; - [Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction.")] + [Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction. Can be used to require the affliction to be caused by a specific character.")] public Identifier SourceCharacter { get; set; } = Identifier.Empty; - [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")] + [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type.")] public LimbType TargetLimb { get; set; } - [Serialize(true, IsPropertySaveable.Yes, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] + [Serialize(true, IsPropertySaveable.Yes, "When set to false, limb-specific afflictions are ignored when not checking a specific limb.")] public bool AllowLimbAfflictions { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes, "Minimum strength of the affliction")] + [Serialize(0.0f, IsPropertySaveable.Yes, "Minimum strength of the affliction.")] public float MinStrength { get; set; } public CheckAfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs index 8c11d1829..d3124db34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConditionalAction.cs @@ -6,20 +6,24 @@ using System.Xml.Linq; namespace Barotrauma { + + /// + /// Checks whether an arbitrary condition is met. The conditionals work the same way as they do in StatusEffects. + /// class CheckConditionalAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target to check.")] public Identifier TargetTag { get; set; } - [Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)] + [Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes, description: "Do all of the conditions need to be met, or is it enough if at least one is? Only valid if there are multiple conditionals.")] public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; } private ImmutableArray Conditionals { get; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds, as well as all the hulls linked to it.")] public Identifier ApplyTagToLinkedHulls { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")] + [Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds.")] public Identifier ApplyTagToHull { get; set; } public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs index 56f6b82e2..2ec488f30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckConnectionAction.cs @@ -4,21 +4,24 @@ using System.Linq; namespace Barotrauma; +/// +/// Check whether a specific connection of an item is wired to a specific kind of connection. +/// class CheckConnectionAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to check.")] public Identifier ItemTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The name of the connection to check on the target item.")] public Identifier ConnectionName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item the connection must be wired to. If omitted, it doesn't matter what the connection is wired to.")] public Identifier ConnectedItemTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The name of the other connection the connection must be wired to. If omitted, it doesn't matter what the connection is wired to.")] public Identifier OtherConnectionName { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching connections for the check to succeed.")] public int MinAmount { get; set; } public CheckConnectionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -37,7 +40,7 @@ class CheckConnectionAction : BinaryOptionAction if (!IsCorrectConnection(connection, ConnectionName)) { continue; } if (ConnectedItemTag.IsEmpty && OtherConnectionName.IsEmpty) { - amount += connection.Wires.Count(); + amount += connection.Wires.Count; if (amount >= MinAmount) { return true; } continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index f56d50e29..91f328b5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -1,21 +1,24 @@ #nullable enable using System; -using System.Linq; namespace Barotrauma { + + /// + /// Can be used to check arbitrary campaign metadata set using . + /// class CheckDataAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the data to check.")] public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The condition that must be met for the check to succeed. Uses the same formatting as conditionals (for example, \"gt 5.2\", \"true\", \"lt 10\".)")] public string Condition { get; set; } = ""; - [Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")] + [Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first. Use this if you know the value is a string.")] public bool ForceString { get; set; } - [Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value")] + [Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value. Meaning that you could for example check whether the value of \"progress_of_some_event\" is larger than \"progress_of_some_other_event\".")] public bool CheckAgainstMetadata { get; set; } protected object? value2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index 727d627dd..779f9b4f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -6,18 +6,22 @@ using System.Linq; namespace Barotrauma { + /// + /// Can be used to do various kinds of checks on items: whether a specific kind of item exists, + /// if it's in a specific character's inventory or in a container, or whether some conditions are met on the item. + /// class CheckItemAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Either the tag of the item(s) we want to check, or a character/container the items are inside.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The target item must have one of these identifiers.")] public string ItemIdentifiers { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The target item must have at least one of these tags.")] public string ItemTags { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of matching items for the check to succeed.")] public int Amount { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] @@ -29,31 +33,27 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the found item(s) when the check succeeds.")] public Identifier ApplyTagToItem { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be equipped for the check to succeed?")] public bool RequireEquipped { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the doesn't need to be directly inside the container/character we're checking, but can be nested inside multiple containers (e.g. in a toolbelt in a character's inventory).")] public bool Recursive { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Can be used to require the item to be in a specific ItemContainer of the target container. For example, the input slots of a fabricator (the first ItemContainer of the fabricator, with an index of 0).")] public int ItemContainerIndex { get; set; } private readonly bool checkPercentage; private float requiredConditionalMatchPercentage; - [Serialize(100.0f, IsPropertySaveable.Yes)] - - /// - /// What percentage of targets do the conditionals need to match for the check to succeed? - /// + [Serialize(100.0f, IsPropertySaveable.Yes, description: "What percentage of targets do the conditionals need to match for the check to succeed?")] public float RequiredConditionalMatchPercentage { get { return requiredConditionalMatchPercentage; } set { requiredConditionalMatchPercentage = MathHelper.Clamp(value, 0.0f, 100.0f); } } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the number of matching items is compared to the number of matching items there were at the start of the round. Only valid if RequiredConditionalMatchPercentage is set.")] public bool CompareToInitialAmount { get; set; } private readonly IReadOnlyList conditionals; @@ -94,12 +94,12 @@ namespace Barotrauma private bool EnoughTargets(int totalTargets, int targetsWithConditionalsMatched) { - if (CompareToInitialAmount) - { - totalTargets = ParentEvent.GetInitialTargetCount(TargetTag); - } if (checkPercentage) { + if (CompareToInitialAmount) + { + totalTargets = ParentEvent.GetInitialTargetCount(TargetTag); + } return MathUtils.Percentage(targetsWithConditionalsMatched, totalTargets) >= RequiredConditionalMatchPercentage; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs index 530a63429..96df67378 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs @@ -3,6 +3,9 @@ using System.Linq; namespace Barotrauma; +/// +/// Check whether a specific mission is currently active, selected for the next round or available. +/// class CheckMissionAction : BinaryOptionAction { public enum MissionType @@ -12,16 +15,16 @@ class CheckMissionAction : BinaryOptionAction Available } - [Serialize(MissionType.Current, IsPropertySaveable.Yes)] + [Serialize(MissionType.Current, IsPropertySaveable.Yes, description: "Does the mission need to be currently active, selected for the next round or available.")] public MissionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission.")] public Identifier MissionIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the mission. Ignored if MissionIdentifier is set.")] public Identifier MissionTag { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching missions for the check to succeed.")] public int MissionCount { get; set; } public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs index ac6eef6b2..2a3b45db1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs @@ -1,16 +1,18 @@ +using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { + /// + /// Check whether the crew or a specific player has enough money. + /// class CheckMoneyAction : BinaryOptionAction { - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "Minimum amount of money the crew or the player must have.")] public int Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the player to check. If omitted, the crew's shared wallet is checked instead.")] public Identifier TargetTag { get; set; } public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs index 15e5e90a9..cc0023ad8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs @@ -1,5 +1,8 @@ namespace Barotrauma; +/// +/// Checks the state of an Objective created using . +/// partial class CheckObjectiveAction : BinaryOptionAction { public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 39b427ba4..9cfd45006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -2,6 +2,9 @@ using Barotrauma.Extensions; namespace Barotrauma { + /// + /// Check whether a specific character has been given a specific order. + /// class CheckOrderAction : BinaryOptionAction { public enum OrderPriority @@ -10,19 +13,19 @@ namespace Barotrauma Any } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the order the target character must have.")] public Identifier OrderIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The option that must be selected for the order. If the order has multiple options (such as turning on or turning off a reactor).")] public Identifier OrderOption { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity the order must be targeting. Only valid for orders that can target a specific entity (such as orders to operate a specific turret).")] public Identifier OrderTargetTag { get; set; } - [Serialize(OrderPriority.Any, IsPropertySaveable.Yes)] + [Serialize(OrderPriority.Any, IsPropertySaveable.Yes, description: "Does the order need to have top priority, or is any priority fine?")] public OrderPriority Priority { get; set; } public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs index b0ac35616..17ce5fa12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs @@ -3,6 +3,9 @@ using System.Linq; namespace Barotrauma; +/// +/// Check whether specific kinds of items have been purchased or sold during the round. +/// class CheckPurchasedItemsAction : BinaryOptionAction { public enum TransactionType @@ -11,16 +14,16 @@ class CheckPurchasedItemsAction : BinaryOptionAction Sold } - [Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)] + [Serialize(TransactionType.Purchased, IsPropertySaveable.Yes, description: "Do the items need to have been purchased or sold?")] public TransactionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item that must have been purchased or sold.")] public Identifier ItemIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must have been purchased or sold.")] public Identifier ItemTag { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Minimum number of matching items that must have been purchased or sold.")] public int MinCount { get; set; } public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs index 478fb872d..dc447ff92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs @@ -3,9 +3,12 @@ using System.Diagnostics; namespace Barotrauma { + /// + /// Check whether the reputation of the crew for a specific faction meets some criteria (e.g. equal to, larger than or less than some value). + /// class CheckReputationAction : CheckDataAction { - [Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes)] + [Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes, description: "Should the action check the reputation for a given faction, or whichever faction owns the current location.")] public ReputationAction.ReputationType TargetType { get; set; } public CheckReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs index 0e673e047..ca3b4795c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckSelectedAction.cs @@ -3,17 +3,20 @@ using System.Collections.Generic; namespace Barotrauma { + /// + /// Check whether a specific character has selected a specific kind of item. + /// class CheckSelectedAction : BinaryOptionAction { public enum SelectedItemType { Primary, Secondary, Any }; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier CharacterTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If specified, only items that have been given this tag using TagAction are considered valid.")] public Identifier TargetTag { get; set; } - [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)] + [Serialize(SelectedItemType.Any, IsPropertySaveable.Yes, description: "How does the item need to be selected? Primary item (i.e. any device you're interacting with), secondary item (such as ladders or chairs which allow interacting with a primary item at the same time), or either?")] public SelectedItemType ItemType { get; set; } public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs index b974c6f83..667cf2d76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTalentAction.cs @@ -2,12 +2,15 @@ namespace Barotrauma { + /// + /// Check whether a specific character has a specific talent. + /// internal sealed class CheckTalentAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the talent to check for.")] public Identifier TalentIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier TargetTag { get; set; } public CheckTalentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs index ab2b9fde6..e4e11c9f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorEventStateAction.cs @@ -2,9 +2,12 @@ namespace Barotrauma { + /// + /// Check the state of the traitor event the action is defined in. Only valid for traitor events. + /// class CheckTraitorEventStateAction : BinaryOptionAction { - [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes, description: "What does the state of the event need to be for the check to succeed?")] public TraitorEvent.State State { get; set; } private readonly TraitorEvent? traitorEvent; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs index 9ab84cd3e..203503e02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckTraitorVoteAction.cs @@ -9,7 +9,7 @@ namespace Barotrauma /// class CheckTraitorVoteAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character to check.")] public Identifier Target { get; set; } public CheckTraitorVoteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs index 119fa0649..13a217c0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckVisibilityAction.cs @@ -4,6 +4,9 @@ using System.Linq; namespace Barotrauma { + /// + /// Check whether a specific entity is visible from the perspective of another entity. + /// class CheckVisibilityAction : BinaryOptionAction { [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs index 4de2f7c4f..ecba9a6b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs @@ -1,10 +1,12 @@ -using System.Xml.Linq; - namespace Barotrauma { + + /// + /// Clears the specific tag from the event (i.e. untagging all the entities that have been previously given the tag). + /// class ClearTagAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The tag to clear.")] public Identifier Tag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs index 9f1242e13..a9cc76a69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs @@ -1,31 +1,33 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Makes an NPC switch to a combat state (with options for different kinds of behaviors, such as offensive, arresting or retreating). + /// class CombatAction : EventAction { - [Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes)] + [Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes, description: $"What kind of combat mode should the NPC switch to (Defensive, Offensive, Arrest, Retreat, None)?")] public AIObjectiveCombat.CombatMode CombatMode { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)?")] + [Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)? Attacking instigators doesn't reduce reputation or trigger outpost security.")] public bool IsInstigator { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes, description: "How do guards react to this character attacking others?")] public AIObjectiveCombat.CombatMode GuardReaction { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes, description: "How do other NPCs react to this character attacking others?")] public AIObjectiveCombat.CombatMode WitnessReaction { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The tag of the NPC to switch to combat mode.")] public Identifier NPCTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character the NPC should attack.")] public Identifier EnemyTag { get; set; } - [Serialize(120.0f, IsPropertySaveable.Yes)] + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How long it takes for the NPC to \"cool down\" (stop attacking).")] public float CoolDown { get; set; } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 7817c012c..93511edb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -8,6 +8,10 @@ using System.Xml.Linq; namespace Barotrauma { + + /// + /// Triggers a "conversation popup" with text and support for different branching options. + /// partial class ConversationAction : EventAction { @@ -26,43 +30,40 @@ namespace Barotrauma /// const float BlockOtherConversationsDuration = 5.0f; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The text to display in the prompt. Can be the text as-is, or a tag referring to a line in a text file.")] public string Text { get; set; } - [Serialize(0, IsPropertySaveable.Yes)] - public int DefaultOption { get; set; } - - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who's speaking. Makes a speech bubble icon appear above the character to indicate you can speak with them, and stops the character in place when the conversation triggers. Also allows the conversation to be interrupted if the speaker dies or becomes incapacitated mid-conversation.")] public Identifier SpeakerTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the player the conversation is shown to. If empty, the conversation is shown to everyone. If SpeakerTag is defined, the conversation is always only shown to the player who interacts with the speaker.")] public Identifier TargetTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, "Should someone interact with the speaker for the conversation to trigger?")] public bool WaitForInteraction { get; set; } - [Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation")] + [Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation.")] public Identifier InvokerTag { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the screen fade to black when the conversation is active?")] public bool FadeToBlack { get; set; } [Serialize(true, IsPropertySaveable.Yes, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")] public bool EndEventIfInterrupted { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of an event sprite to display in the corner of the conversation prompt.")] public string EventSprite { get; set; } - [Serialize(DialogTypes.Regular, IsPropertySaveable.Yes)] + [Serialize(DialogTypes.Regular, IsPropertySaveable.Yes, description: "Type of the dialog prompt.")] public DialogTypes DialogType { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Does this conversation continue after this ConversationAction? If you have multiple successive ConversationActions, perhaps with some actions happening in between, you can enable this to prevent the dialog prompt from closing between the actions. Not necessary if the ConversationActions are nested inside each other: those are always considered parts of the same conversation, and shown in the same prompt.")] public bool ContinueConversation { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")] public bool ContinueAutomatically { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If SpeakerTag is defined, the conversation is interrupted by default if the speaker and the target end up too far from each other. This can be used to disable that behavior, keeping the dialog prompt open regardless of the distance.")] public bool IgnoreInterruptDistance { get; set; } public Character Speaker @@ -72,6 +73,7 @@ namespace Barotrauma } private AIObjective prevIdleObjective, prevGotoObjective; + private AIObjective npcWaitObjective; public List Options { get; private set; } @@ -275,6 +277,10 @@ namespace Barotrauma if (!SpeakerTag.IsEmpty) { + if (npcWaitObjective != null) + { + npcWaitObjective.ForceHighestPriority = true; + } if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; if (Speaker == null || Speaker.Removed) @@ -386,11 +392,11 @@ namespace Barotrauma { prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); prevGotoObjective = humanAI.ObjectiveManager.GetObjective(); - humanAI.SetForcedOrder( + npcWaitObjective = humanAI.SetForcedOrder( new Order(OrderPrefab.Prefabs["wait"], Barotrauma.Identifier.Empty, null, orderGiver: null)); - if (targets.Any()) + if (targets.Any() || targetCharacter != null) { - Entity closestTarget = null; + Entity closestTarget = targetCharacter; float closestDist = float.MaxValue; foreach (Entity entity in targets) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs index 70b6f74b9..13d3b6859 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CountTargetsAction.cs @@ -5,9 +5,12 @@ using System.Linq; namespace Barotrauma { + /// + /// Check whether there's at least / at most some number of entities matching some specific criteria. + /// class CountTargetsAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entities to check.")] public Identifier TargetTag { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "Optional second tag. Can be used if the target must have two different tags.")] @@ -16,29 +19,19 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")] public Identifier HullTag { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Minimum number of matching entities for the check to succeed. If omitted or negative, there is no minimum amount.")] public int MinAmount { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of matching entities for the check to succeed. If omitted or negative, there is no maximum amount.")] public int MaxAmount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of some other entities to compare the number of targets to. E.g. you could compare the number of entities tagged as \"discoveredhull\" to entities tagged as \"anyhull\". The minimum/maximum amount of entities there must be relative to the other entities is configured using MinPercentageRelativeToTarget and MaxPercentageRelativeToTarget.")] public Identifier CompareToTarget { get; set; } - [Serialize(-1.0f, IsPropertySaveable.Yes)] - - /// - /// Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget - /// E.g. you could compare the number of entities tagged as "discoveredhull" to entities tagged as "anyhull" to require 50% of hulls to be discovered. - /// + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Minimum amount of targets, as a percentage of the number of entities tagged with CompareToTarget. E.g. you could compare the number of entities tagged as \"discoveredhull\" to entities tagged as \"anyhull\" to require 50% of hulls to be discovered.")] public float MinPercentageRelativeToTarget { get; set; } - [Serialize(-1.0f, IsPropertySaveable.Yes)] - - /// - /// Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget - /// E.g. you could compare the number of entities tagged as "floodedhull" to entities tagged as "anyhull" to require less than 50% of hulls to be flooded. - /// + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum amount of targets, as a percentage of the number of entities tagged with CompareToTarget. E.g. you could compare the number of entities tagged as \"floodedhull\" to entities tagged as \"anyhull\" to require less than 50% of hulls to be flooded.")] public float MaxPercentageRelativeToTarget { get; set; } private readonly IReadOnlyList conditionals; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs index c6f24003d..5bbd67774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventLogAction.cs @@ -5,15 +5,19 @@ using System.Xml.Linq; namespace Barotrauma { + + /// + /// Adds an entry to the "event log" displayed in the mission tab of the tab menu. + /// partial class EventLogAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the entry. If there's already an entry with the same id, it gets overwritten.")] public Identifier Id { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Text to add to the event log. Can be the text as-is, or a tag referring to a line in a text file.")] public string Text { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) who should see the entry. If empty, the entry is shown to everyone.")] public Identifier TargetTag { get; set; } public bool ShowInServerLog { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs index 2faa398bf..dceb01b73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventObjectiveAction.cs @@ -1,40 +1,81 @@ +using System; + namespace Barotrauma { + + /// + /// Displays an objective in the top-right corner of the screen, or modifies an existing objective in some way. + /// partial class EventObjectiveAction : EventAction { - public enum SegmentActionType { Trigger, Add, AddIfNotFound, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove }; + public enum SegmentActionType + { + /// + /// Legacy support. Triggers an info box segment, with optional support for video clips. + /// + [Obsolete] + Trigger, + /// + /// Adds a new objective to the list. + /// + Add, + /// + /// Adds a new objective to the list if there are no existing objectives with the same identifier. + /// + AddIfNotFound, + /// + /// Marks the objective as completed. + /// + Complete, + /// + /// Marks the objective as completed and removes it from the list. + /// + CompleteAndRemove, + /// + /// Removes the objective from the list. + /// + Remove, + /// + /// Marks the objective as failed. + /// + Fail, + /// + /// Marks the objective as failed and removes it from the list. + /// + FailAndRemove + }; - [Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)] + [Serialize(SegmentActionType.Add, IsPropertySaveable.Yes, description: "Should the action add a new objective, or do something to an existing objective?")] public SegmentActionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Arbitrary identifier given to the objective. Can be used to complete/remove/fail the objective later. Also used to fetch the text from the text files.")] public Identifier Identifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Obsolete, Serialize("", IsPropertySaveable.Yes, description: "Legacy support. Tag of the text to display as an objective in info box segments.")] public Identifier ObjectiveTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Obsolete, Serialize(true, IsPropertySaveable.Yes, description: "Legacy support. Is this objective possible to complete if it's used in an info box segment.")] public bool CanBeCompleted { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of a parent objective. If set, this objective is displayed as a subobjective under the parent objective.")] public Identifier ParentObjectiveId { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Obsolete, Serialize(false, IsPropertySaveable.Yes, description: "Legacy support. Should the video defined by VideoFile play automatically, or wait for the user to play it.")] public bool AutoPlayVideo { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Obsolete, Serialize("", IsPropertySaveable.Yes, description: "Legacy support. Tag of the main text to display in info box segments.")] public Identifier TextTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Obsolete, Serialize("", IsPropertySaveable.Yes, description: "Legacy support. Path of a video file to display in info box segments.")] public string VideoFile { get; set; } - [Serialize(450, IsPropertySaveable.Yes)] + [Obsolete, Serialize(450, IsPropertySaveable.Yes, description: "Legacy support. Width of the info box segment.")] public int Width { get; set; } - [Serialize(80, IsPropertySaveable.Yes)] + [Obsolete, Serialize(80, IsPropertySaveable.Yes, description: "Legacy support. Height of the info box segment.")] public int Height { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) to show the objective to.")] public Identifier TargetTag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs index 0adf73e69..87ab4de2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs @@ -1,17 +1,16 @@ using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Starts a fire at the position of a specific target. + /// class FireAction : EventAction { - [Serialize(10.0f, IsPropertySaveable.Yes)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "Size of the fire (width in pixels).")] public float Size { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to start the fire at.")] public Identifier TargetTag { get; set; } public FireAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs index f2899ff8a..dad761ed0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveExpAction.cs @@ -2,12 +2,15 @@ using System.Linq; namespace Barotrauma { + /// + /// Gives experience to a specific character. + /// class GiveExpAction : EventAction { - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "The amount of experience to give. Cannot be negative.")] public int Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) to give the experience to.")] public Identifier TargetTag { get; set; } public GiveExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 8f8cc7d0b..92f878f9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -2,15 +2,18 @@ using System.Linq; namespace Barotrauma { + /// + /// Increases the skill level of a specific character. + /// class GiveSkillExpAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the skill to increase.")] public Identifier Skill { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the skill should increase.")] public float Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) whose skill to increase.")] public Identifier TargetTag { get; set; } public GiveSkillExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index d2cb4011f..bc83b8afd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -1,11 +1,14 @@ namespace Barotrauma { + /// + /// Makes the event jump to a somewhere else in the event. + /// class GoTo : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Name of the label to jump to.")] public string Name { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "How many times can this GoTo action be repeated? Can be used to make some parts of an event repeat a limited number of times. If negative or zero, there's no limit.")] public int MaxTimes { get; set; } private int counter; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs index e9995190f..f029d9b1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs @@ -1,14 +1,17 @@ namespace Barotrauma { + /// + /// Makes a specific character invulnerable to damage and unable to die. + /// class GodModeAction : EventAction { - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the godmode be enabled or disabled?")] public bool Enabled { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should the character's active afflictions be updated (e.g. applying visual effects of the afflictions)")] public bool UpdateAfflictions { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character whose godmode to enable/disable.")] public Identifier TargetTag { get; set; } public GodModeAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs index 8900299ea..688212ddb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/HighlightAction.cs @@ -5,17 +5,20 @@ using System.Linq; namespace Barotrauma; +/// +/// Highlights a specific entity. +/// partial class HighlightAction : EventAction { private static readonly Color highlightColor = Color.Orange; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to highlight.")] public Identifier TargetTag { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "Only the player controlling this character will see the highlight. If empty, all players will see it.")] public Identifier TargetCharacter { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the highlight be turned on or off?")] public bool State { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs index 7eecbbbb2..272bd6c5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/InventoryHighlightAction.cs @@ -1,17 +1,20 @@ namespace Barotrauma; +/// +/// Highlights specific items in a specific inventory. +/// partial class InventoryHighlightAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity or entities whose inventory the item should be highlighted in. Must be a character or an item with an inventory.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item(s) to highlight.")] public Identifier ItemIdentifier { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "If the target is an item with multiple ItemContainer components (i.e. multiple inventories), such as a fabricator, this determines which inventory to highlight the item in (0 = first, 1 = second). If negative, it doesn't matter which inventory the item is in.")] public int ItemContainerIndex { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the action will go look through all the containers in the target inventory (e.g. highlighting a tank in a welding tool in the target inventory).")] public bool Recursive { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs index a95a7dea7..0dd3e3c70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs @@ -1,7 +1,8 @@ -using System.Xml.Linq; - namespace Barotrauma { + /// + /// Defines a point in the event that actions can jump to. + /// class Label : EventAction { [Serialize("", IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/LayerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/LayerAction.cs new file mode 100644 index 000000000..5012cde09 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/LayerAction.cs @@ -0,0 +1,61 @@ +namespace Barotrauma; + +/// +/// Enable or disable a specific layer in a specific submarine. +/// +class LayerAction : EventAction +{ + [Serialize("", IsPropertySaveable.Yes, description: "Which layer to enable/disable. Use \"All\" to apply it to all layers.")] + public Identifier Layer { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "Whether to enable or disable the layer.")] + public bool Enabled { get; set; } + + [Serialize(TagAction.SubType.Any, IsPropertySaveable.Yes, description: "The type of submatine to enable or disable the layer in.")] + public TagAction.SubType SubmarineType { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should the action continue if it can't find the specified layer in the specified submarine(s).")] + public bool ContinueIfNotFound { get; set; } + + public LayerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + private bool isFinished; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + bool layerFound = false; + foreach (var submarine in Submarine.Loaded) + { + if (!TagAction.SubmarineTypeMatches(submarine, SubmarineType)) { continue; } + if (submarine.LayerExists(Layer)) + { + submarine.SetLayerEnabled(Layer, Enabled, sendNetworkEvent: true); + layerFound = true; + } + } + if (ContinueIfNotFound) + { + isFinished = true; + } + else + { + if (layerFound) { isFinished = true; } + } + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(LayerAction)} -> ({(Enabled ? "Enable" : "Disable")} {Layer.ColorizeObject()})"; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs index 6ac082e66..8c34bcd8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs @@ -1,52 +1,55 @@ namespace Barotrauma { + /// + /// Displays a message box, or modifies an existing one. + /// partial class MessageBoxAction : EventAction { public enum ActionType { Create, ConnectObjective, Close, Clear } - [Serialize(ActionType.Create, IsPropertySaveable.Yes)] + [Serialize(ActionType.Create, IsPropertySaveable.Yes, description: "What do you want to do with the message box (Create, ConnectObjective, Close, Clear)?")] public ActionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional identifier of the tutorial \"segment\" that can be referenced by other event actions.")] public Identifier Identifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "An arbitrary tag given to the message box. Only required if you're intending to close or clear the box with another MessageBoxAction later.")] public string Tag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Text displayed in the header of the message box. Can be either the text as-is, or a tag referring to a line in a text file.")] public Identifier Header { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Text displayed in the body of the message box. Can be either the text as-is, or a tag referring to a line in a text file.")] public Identifier Text { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Style of the icon displayed in the corner of the message box (optional). The style must be defined in a UIStyle file.")] public string IconStyle { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the button that closes the box be hidden? If it is hidden, you must close the box manually using another MessageBoxAction.")] public bool HideCloseButton { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) to show the message box to.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed on some input (e.g. Select, Use, CrewOrders).")] public string CloseOnInput { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user selects an item that has this tag.")] public Identifier CloseOnSelectTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user picks up an item that has this tag.")] public Identifier CloseOnPickUpTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user equips an item that has this tag.")] public Identifier CloseOnEquipTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user exits a room with this name.")] public Identifier CloseOnExitRoomName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The message box is automatically closed when the user is in a room with this name.")] public Identifier CloseOnInRoomName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag that will be used to get the text for the objective that is displayed on the screen.")] public Identifier ObjectiveTag { get; set; } [Serialize(true, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 4592e6751..a423ce459 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -7,15 +7,18 @@ using System.Linq; namespace Barotrauma { + /// + /// Unlocks a mission in a nearby level or location. + /// partial class MissionAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission to unlock.")] public Identifier MissionIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the mission to unlock. If there are multiple missions with the tag, one is chosen randomly.")] public Identifier MissionTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The mission can only be unlocked in a location that's occupied by this faction.")] public Identifier RequiredFaction { get; set; } public ImmutableArray LocationTypes { get; } @@ -46,7 +49,14 @@ namespace Barotrauma contentPackage: element.ContentPackage); } LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); - random = new MTRandom(parentEvent.RandomSeed); + //the action chooses the same mission if + // 1. event seed is the same (based on level seed, changes when events are completed) + // 2. event is the same (two different events shouldn't choose the same mission) + // 3. the MissionAction is the same (two different actions in the same event shouldn't choose the same mission) + random = new MTRandom( + parentEvent.RandomSeed + + ToolBox.StringToInt(ParentEvent.Prefab.Identifier.Value) + + ParentEvent.Actions.Count); } public override bool IsFinished(ref string goTo) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs index 02b726aa8..98dcfd8f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -1,8 +1,12 @@ namespace Barotrauma { + + /// + /// Changes the state of a specific active mission. The way the states are used depends on the type of mission. + /// class MissionStateAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the mission whose state to change.")] public Identifier MissionIdentifier { get; set; } public enum OperationType @@ -11,10 +15,10 @@ namespace Barotrauma Add } - [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Should the value be added to the state of the mission, or should the state be set to the specified value.")] public OperationType Operation { get; set; } - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")] public int State { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs index a21925c94..4b35c6c46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -1,17 +1,20 @@ namespace Barotrauma { + /// + /// Modifies the current location in some way (e.g. adjusting the faction, type of name). + /// class ModifyLocationAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the faction to set as the location's primary faction (optional).")] public Identifier Faction { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the faction to set as the location's secondary faction (optional).")] public Identifier SecondaryFaction { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the location type to set as the location's new type (optional)")] public Identifier Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "New name to give to the location (optional). Can either be the name as-is, or a tag referring to a line in a text file.")] public Identifier Name { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index c3030d6df..b6d4331a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -1,20 +1,21 @@ -using System; +using Barotrauma.Networking; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { + /// + /// Give or remove money from the crew or a specific character. + /// class MoneyAction : EventAction { public MoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0, IsPropertySaveable.Yes)] + [Serialize(0, IsPropertySaveable.Yes, description: "Amount of money to give or remove.")] public int Amount { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If set, the money is removed from character(s) with this tag.")] public Identifier TargetTag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 239b95f60..7269d169f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -4,20 +4,21 @@ using System.Linq; namespace Barotrauma { + /// + /// Changes the team of an NPC. Most common use cases are adding a character to the crew, or turning an NPC hostile to the crew by changing their team to a hostile one. + /// class NPCChangeTeamAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) whose team to change.")] public Identifier NPCTag { get; set; } - [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: "The team to move the NPC to. None = unspecified, Team1 = player crew, Team2 = the team opposing Team1 (= hostile to player crew), FriendlyNPC = friendly to all other teams.")] public CharacterTeamType TeamID { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be added to the player crew?")] public bool AddToCrew { get; set; } - - - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the NPC be removed from the player crew?")] public bool RemoveFromCrew { get; set; } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 94111f0fd..2b01611ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -3,21 +3,24 @@ using System.Linq; namespace Barotrauma { + /// + /// Makes an NPC follow or stop following a specific target. + /// class NPCFollowAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) that should follow the target.")] public Identifier NPCTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target. Can be any type of entity: if it's a static one like a device or a hull, the NPC will just stay at the position of that target.")] public Identifier TargetTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop following the target?")] public bool Follow { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs to target (e.g. you could choose to only make a specific number of security officers follow the player.)")] public int MaxTargets { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop following the target when the event resets?")] public bool AbandonOnReset { get; set; } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs index e3e8c9ade..d19cd7c52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCOperateItemAction.cs @@ -1,39 +1,43 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; namespace Barotrauma { + /// + /// Makes an NPC select an item, and operate it if it's something AI characters can operate. + /// class NPCOperateItemAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) that should operate the item.")] public Identifier NPCTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to operate. If it's not something AI characters can or know how to operate, such as a cabinet or an engine, the NPC will just select it.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("Controller", IsPropertySaveable.Yes, description: "Name of the component to operate. For example, the Controller component of a periscope or the Reactor component of a nuclear reactor.")] public Identifier ItemComponentName { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the option, if there are several ways the item can be operated. For example, \"powerup\" or \"shutdown\" when operating a reactor.")] public Identifier OrderOption { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the character equip the item before attempting to operate it (only valid if the item is equippable).")] public bool RequireEquip { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the character start or stop operating the item.")] public bool Operate { get; set; } - [Serialize(-1, IsPropertySaveable.Yes)] + [Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs the action can target. For example, you could only make a specific number of security officers man a periscope.")] public int MaxTargets { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop operating the item when the event resets?")] public bool AbandonOnReset { get; set; } private bool isFinished = false; - + public NPCOperateItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - private List affectedNpcs = null; private Item target = null; @@ -41,7 +45,13 @@ namespace Barotrauma { if (isFinished) { return; } - target = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Item; + var potentialTargets = ParentEvent.GetTargets(TargetTag).OfType(); + var nonSelectedItems = potentialTargets.Where(it => it.GetComponent()?.User == null); + + target = + nonSelectedItems.Any() ? + nonSelectedItems.GetRandomUnsynced() : + potentialTargets.GetRandomUnsynced(); if (target == null) { return; } int targetCount = 0; @@ -53,7 +63,6 @@ namespace Barotrauma if (Operate) { - ItemComponentName = "Controller".ToIdentifier(); var itemComponent = target.Components.FirstOrDefault(ic => ItemComponentName == ic.Name); if (itemComponent == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 28db501f7..8a12868f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -1,15 +1,17 @@ using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Makes an NPC stop and wait. + /// class NPCWaitAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the NPC(s) that should wait.")] public Identifier NPCTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop waiting?")] public bool Wait { get; set; } private bool isFinished = false; @@ -35,6 +37,7 @@ namespace Barotrauma var gotoObjective = new AIObjectiveGoTo( AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true) { + FaceTargetOnCompleted = false, OverridePriority = 100.0f, SourceEventAction = this, IsWaitOrder = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs index 23bb3b63a..baf4fba0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/OnRoundEndAction.cs @@ -2,6 +2,9 @@ namespace Barotrauma { + /// + /// Executes all the child actions when the round ends. + /// class OnRoundEndAction : EventAction { private readonly SubactionGroup subActions; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index a9e4ffea3..163f6f65c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; - namespace Barotrauma { + /// + /// Randomly executes either of the child actions (Success or Failure). + /// class RNGAction : BinaryOptionAction { - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The probability of executing the Success actions. A value between 0-1.")] public float Chance { get; set; } public RNGAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index 348e856e8..75c446b46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -3,15 +3,18 @@ using System.Collections.Immutable; namespace Barotrauma { + /// + /// Removes (deletes) a specific item or items. + /// class RemoveItemAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item(s) to remove.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional list of identifiers the item(s) must have. You might for example want to go through all tagged items inside a cabinet, but only remove specific types of items.")] public string ItemIdentifiers { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Maximum number of items to remove.")] public int Amount { get; set; } private readonly ImmutableHashSet itemIdentifierSplit; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index f161a63fa..3298f8312 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma +namespace Barotrauma { + /// + /// Adjusts the crew's reputation by some value. + /// class ReputationAction : EventAction { public enum ReputationType @@ -17,13 +14,13 @@ namespace Barotrauma public ReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Amount of reputation to add or remove.")] public float Increase { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the faction you want to adjust the reputation for. Ignored if TargetType is set to Location.")] public Identifier Identifier { get; set; } - [Serialize(ReputationType.None, IsPropertySaveable.Yes)] + [Serialize(ReputationType.None, IsPropertySaveable.Yes, description: "Do you want to adjust the reputation for a specific faction, or whichever faction controls the current location?")] public ReputationType TargetType { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs index 7e33a2373..4257430d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs @@ -1,8 +1,10 @@ using System; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Sets a campaign metadata value. The metadata can be any arbitrary data you want to save: for example, whether some event has been completed, the number of times something has been done during the campaign, or at what stage of some multi-part event chain the crew is at. + /// class SetDataAction : EventAction { public enum OperationType @@ -14,13 +16,13 @@ namespace Barotrauma public SetDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Do you want to set the metadata to a specific value, multiply it, or add to it.")] public OperationType Operation { get; set; } - [Serialize(null, IsPropertySaveable.Yes)] + [Serialize(null, IsPropertySaveable.Yes, description: "Depending on the operation, the value you want to set the metadata to, multiply it with, or add to it.")] public string Value { get; set; } = null!; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the metadata to set. Can be any arbitrary identifier, e.g. itemscollected, my_custom_event_state, specialnpckilled...")] public Identifier Identifier { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs index bdded765e..5b153da11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs @@ -1,8 +1,10 @@ using System; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Adjusts the price multiplier for stores or mechanical repairs in the current location. + /// class SetPriceMultiplierAction : EventAction { public enum OperationType @@ -19,13 +21,13 @@ namespace Barotrauma Mechanical } - [Serialize(1.0f, IsPropertySaveable.Yes)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Value to set as the multiplier, or to multiply, min or max the current multiplier with.")] public float Multiplier { get; set; } - [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes, description: "Do you want to set the value as the multiplier, multiply the existing multiplier with it, or take the smaller or larger of the values.")] public OperationType Operation { get; set; } - [Serialize(PriceMultiplierType.Store, IsPropertySaveable.Yes)] + [Serialize(PriceMultiplierType.Store, IsPropertySaveable.Yes, description: "Do you want to set the price multiplier for stores or for mechanical services (hull and item repairs and restoring lost shuttles)?")] public PriceMultiplierType TargetMultiplier { get; set; } public SetPriceMultiplierAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs index af798fefc..f4b402ca4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetTraitorEventStateAction.cs @@ -2,6 +2,9 @@ namespace Barotrauma { + /// + /// Sets the state of the traitor event. Only valid in traitor events. + /// class SetTraitorEventStateAction : EventAction { private readonly TraitorEvent? traitorEvent; @@ -19,7 +22,7 @@ namespace Barotrauma } } - [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)] + [Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes, description: "The state to set the traitor event to (Incomplete, Completed or Failed).")] public TraitorEvent.State State { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index b3376e4a3..10d7f7dff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -1,22 +1,22 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Performs a skill check and executes either the Success or Failure child actions depending on whether the check succeeds. + /// class SkillCheckAction : BinaryOptionAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The identifier of the skill to check.")] public Identifier RequiredSkill { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The required skill level for the check to succeed.")] public float RequiredLevel { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the skill check be probability-based (i.e. if you have half the required skill level, the chance of success is 50%), or should the check always fail when under the required level and always succeed when above? ")] public bool ProbabilityBased { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character(s) whose skill to check. If there are multiple targets, the action succeeds if any of their skill checks succeeds.")] public Identifier TargetTag { get; set; } public SkillCheckAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 15a58b8c0..507d547bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -6,6 +6,9 @@ using System.Linq; namespace Barotrauma { + /// + /// Spawns an entity (e.g. item, NPC, monster). + /// class SpawnAction : EventAction { public enum SpawnLocationType @@ -41,16 +44,16 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")] public Identifier TargetInventory { get; set; } - [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes)] + [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes, description: "Where should the entity spawn? This can be restricted further with the other spawn point options.")] public SpawnLocationType SpawnLocation { get; set; } - [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] + [Serialize(SpawnType.Human, IsPropertySaveable.Yes, description: "Type of spawnpoint to spawn the entity at. Ignored if SpawnPointTag is set.")] public SpawnType SpawnPointType { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of a spawnpoint to spawn the entity at.")] public Identifier SpawnPointTag { get; set; } - [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)] + [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes, description: "Team of the NPC to spawn. Only valid when spawning a character.")] public CharacterTeamType TeamID { get; protected set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] @@ -61,10 +64,10 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes, description: "If false, we won't spawn another character if one with the same identifier has already been spawned.")] public bool AllowDuplicates { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Number of entities to spawn.")] public int Amount { get; set; } - [Serialize(100.0f, IsPropertySaveable.Yes)] + [Serialize(100.0f, IsPropertySaveable.Yes, description: "Random offset to add to the spawn position.")] public float Offset { get; set; } [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index f07c316cd..2035c53c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -2,13 +2,16 @@ using System.Collections.Generic; namespace Barotrauma { + /// + /// Executes all the StatusEffects defined as child elements of the action. + /// partial class StatusEffectAction : EventAction { private readonly List effects = new List(); private readonly int actionIndex; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity or entities the status effect should target.")] public Identifier TargetTag { get; set; } public StatusEffectAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 8a1e389fe..a2046bd0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -1,4 +1,4 @@ -using Barotrauma.Extensions; +using Barotrauma.Extensions; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -6,26 +6,32 @@ using System.Linq; namespace Barotrauma { + /// + /// Tags a specific entity. Tags are used by other actions to refer to specific entities. The tags are event-specific, i.e. you cannot use a tag that was added by another event to refer to an entity. + /// class TagAction : EventAction { public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag].")] public string Criteria { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "The tag to apply to the target.")] public Identifier Tag { get; set; } - [Serialize(SubType.Any, IsPropertySaveable.Yes)] + [Serialize(SubType.Any, IsPropertySaveable.Yes, description: "The type of submarine the target needs to be in.")] public SubType SubmarineType { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, "If set, the target must be in an outpost module that has this tag.")] + public Identifier RequiredModuleTag { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Should incapacitated (e.g. dead, paralyzed, unconscious) characters be ignored, i.e. not considered valid targets?")] public bool IgnoreIncapacitatedCharacters { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Can items that have been set to be hidden in-game be tagged?")] public bool AllowHiddenItems { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If there are multiple matching targets, should all of them be tagged or one chosen randomly?")] public bool ChooseRandom { get; set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue if the TagAction can't find any valid targets?")] @@ -85,7 +91,7 @@ namespace Barotrauma private void TagByEventTag(Identifier eventTag) { - AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => SubmarineTypeMatches(t.Submarine))); + AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => MatchesRequirements(t))); } private void TagPlayers() @@ -157,7 +163,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Structure, - e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); + e => e is Structure s && MatchesRequirements(s) && s.Prefab.Identifier == identifier); } private void TagStructuresBySpecialTag(Identifier tag) @@ -165,7 +171,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Structure, - e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag); + e => e is Structure s && MatchesRequirements(s) && s.SpecialTag.ToIdentifier() == tag); } private void TagItemsByIdentifier(Identifier identifier) @@ -173,7 +179,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Item, - e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); + e => e is Item it && it.Prefab.Identifier == identifier && IsValidItem(it)); } private void TagItemsByTag(Identifier tag) @@ -181,7 +187,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Item, - e => e is Item it && IsValidItem(it) && it.HasTag(tag)); + e => e is Item it && it.HasTag(tag) && IsValidItem(it)); } private void TagHulls() @@ -189,7 +195,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Hull, - e => e is Hull h && SubmarineTypeMatches(h.Submarine)); + e => e is Hull h && MatchesRequirements(h)); } private void TagHullsByName(Identifier name) @@ -197,7 +203,7 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Hull, - e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); + e => e is Hull h && MatchesRequirements(h) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private void TagSubmarinesByType(Identifier type) @@ -205,33 +211,76 @@ namespace Barotrauma AddTargetPredicate( Tag, ScriptedEvent.TargetPredicate.EntityType.Submarine, - e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + e => e is Submarine s && MatchesRequirements(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } private bool IsValidItem(Item it) { return (!it.HiddenInGame || AllowHiddenItems) && + ModuleTagMatches(it) && //if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet //= it.Submarine still null SubmarineTypeMatches(it.Submarine ?? it.CurrentHull?.Submarine ?? it.ParentInventory?.Owner?.Submarine); } + private bool MatchesRequirements(Entity e) + { + return ModuleTagMatches(e) && SubmarineTypeMatches(e.Submarine); + } + + private bool ModuleTagMatches(Entity e) + { + if (RequiredModuleTag.IsEmpty) { return true; } + if (e?.Submarine == null) { return false; } + + Hull hull; + if (e is Character character) + { + hull = character.CurrentHull; + } + else if (e is Item item) + { + hull = item.CurrentHull; + } + else if (e is WayPoint wp) + { + hull = wp.CurrentHull; + } + else if (e is Hull h) + { + hull = h; + } + else + { + DebugConsole.AddWarning($"Potential error in event \"{ParentEvent.Prefab.Identifier}\": {nameof(TagAction)} cannot check the module tags of an entity of the type {e.GetType()}."); + return false; + } + + return hull != null && hull.OutpostModuleTags.Contains(RequiredModuleTag); + } + + private bool SubmarineTypeMatches(Submarine sub) { - if (SubmarineType == SubType.Any) { return true; } + return SubmarineTypeMatches(sub, SubmarineType); + } + + public static bool SubmarineTypeMatches(Submarine sub, SubType submarineType) + { + if (submarineType == SubType.Any) { return true; } if (sub == null) { return false; } switch (sub.Info.Type) { case Barotrauma.SubmarineType.Player: - return SubmarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle; + return submarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle; case Barotrauma.SubmarineType.Outpost: case Barotrauma.SubmarineType.OutpostModule: - return SubmarineType.HasFlag(SubType.Outpost); + return submarineType.HasFlag(SubType.Outpost); case Barotrauma.SubmarineType.Wreck: - return SubmarineType.HasFlag(SubType.Wreck); + return submarineType.HasFlag(SubType.Wreck); case Barotrauma.SubmarineType.BeaconStation: - return SubmarineType.HasFlag(SubType.BeaconStation); + return submarineType.HasFlag(SubType.BeaconStation); default: return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs index 1aacece60..407de994d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TeleportAction.cs @@ -1,19 +1,22 @@ namespace Barotrauma; +/// +/// Teleports a specific entity to a specific spawn point. +/// class TeleportAction : EventAction { public enum TeleportPosition { MainSub, Outpost } - [Serialize(TeleportPosition.MainSub, IsPropertySaveable.Yes)] + [Serialize(TeleportPosition.MainSub, IsPropertySaveable.Yes, description: "Should the entity be teleported to the main submarine or the outpost?")] public TeleportPosition Position { get; set; } - [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] + [Serialize(SpawnType.Human, IsPropertySaveable.Yes, description: "The type of the spawnpoint to teleport the character to.")] public SpawnType SpawnType { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag of the spawnpoint.")] public string SpawnPointTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target(s) to teleport.")] public Identifier TargetTag { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 5c8bce3d0..69b1b1a21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -1,10 +1,12 @@ -using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; namespace Barotrauma { + /// + /// Waits for a player to trigger the action before continuing. Triggering can mean entering a specific trigger area, or interacting with a specific entity. + /// class TriggerAction : EventAction { public enum TriggerType diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index ae6a8c75c..e75d563c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -1,11 +1,14 @@ namespace Barotrauma { + /// + /// Triggers another scripted event. + /// class TriggerEventAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the event to trigger.")] public Identifier Identifier { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the event will trigger at the beginning of the next round. Useful for e.g. triggering some scripted event in the outpost after you finish a mission.")] public bool NextRound { get; set; } private bool isFinished; @@ -41,7 +44,7 @@ } else { - var ev = eventPrefab.CreateInstance(); + var ev = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); if (ev != null) { GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs index 9544f9371..33ec26657 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialCompleteAction.cs @@ -1,5 +1,8 @@ namespace Barotrauma { + /// + /// Completes the tutorial. Only valid in tutorial events. + /// class TutorialCompleteAction : EventAction { private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs index a47d4e932..cf0316278 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialIconAction.cs @@ -2,17 +2,20 @@ using System.Linq; namespace Barotrauma; +/// +/// Displays a tutorial icon next to a specific target. +/// class TutorialIconAction : EventAction { public enum ActionType { Add, Remove, RemoveTarget, RemoveIcon, Clear }; - [Serialize(ActionType.Add, IsPropertySaveable.Yes)] + [Serialize(ActionType.Add, IsPropertySaveable.Yes, description: "What to do with the icon. Add = add an icon, Remove = remove the icon that has the specific target and style, RemoveTarget = remove all icons assigned to the specific target, RemoveIcon = remove all icons with the specific style, Remove = remove all icons.")] public ActionType Type { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the target to assign the icon to.")] public Identifier TargetTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Style of the icon.")] public Identifier IconStyle { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs index ac2beeebb..eeb4c1b72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UIHighlightAction.cs @@ -1,5 +1,8 @@ namespace Barotrauma; +/// +/// Highlights an UI element of some kind. Generally used in tutorials. +/// partial class UIHighlightAction : EventAction { public enum ElementId @@ -24,28 +27,28 @@ partial class UIHighlightAction : EventAction MessageBoxCloseButton } - [Serialize(ElementId.None, IsPropertySaveable.Yes)] + [Serialize(ElementId.None, IsPropertySaveable.Yes, description: "An arbitrary identifier that must match the userdata of the UI element. The userdatas of the element are hard-coded, so this option is generally intended for the developers' use.")] public ElementId Id { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an entity or an entity prefab, it's identifier must match this value.")] public Identifier EntityIdentifier { get; set; } - [Serialize(OrderCategory.Emergency, IsPropertySaveable.Yes)] + [Serialize(OrderCategory.Emergency, IsPropertySaveable.Yes, description: "If the element's userdata is an order category, it must match this.")] public OrderCategory OrderCategory { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an order, it must match this identifier.")] public Identifier OrderIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an order with options, it must match this.")] public Identifier OrderOption { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "If the element's userdata is an order, the order must target an entity with this tag.")] public Identifier OrderTargetTag { get; set; } - [Serialize(true, IsPropertySaveable.Yes)] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the element bounce up an down in addition to being highlighted.")] public bool Bounce { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the action highlight the first matching element it finds, or all of them?")] public bool HighlightMultiple { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs index a69ee509a..eaefc7169 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -1,12 +1,12 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma { + /// + /// Unlocks a "locked" pathways between locations, if there are any such paths adjacent to the current location. + /// class UnlockPathAction : EventAction { public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs index 202a8734d..dcbefe305 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs @@ -1,8 +1,11 @@ namespace Barotrauma { + /// + /// Waits for a specific amount of time before continuing the execution of the event. + /// class WaitAction : EventAction { - [Serialize(0.0f, IsPropertySaveable.Yes)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How long to wait (in seconds).")] public float Time { get; set; } private float timeRemaining; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs index 89106db36..981ce8bd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemFabricatedAction.cs @@ -1,22 +1,25 @@ -#nullable enable +#nullable enable using Barotrauma.Items.Components; using System.Linq; namespace Barotrauma { + /// + /// Waits for some item(s) to be fabricated before continuing the execution of the event. + /// class WaitForItemFabricatedAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character who must fabricate the item. If empty, it doesn't matter who fabricates it.")] public Identifier CharacterTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item that must be fabricated. Optional if ItemTag is set.")] public Identifier ItemIdentifier { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must be fabricated. Optional if ItemIdentifier is set.")] public Identifier ItemTag { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "Number of items that need to be fabricated.")] public int Amount { get; set; } [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the fabricated item(s).")] @@ -48,7 +51,8 @@ namespace Barotrauma { if (!ParentEvent.GetTargets(CharacterTag).Contains(character)) { return; } } - if (item.ContainerIdentifier == ItemTag || item.HasTag(ItemTag)) + if ((!ItemIdentifier.IsEmpty && item.Prefab.Identifier == ItemIdentifier) || + (!ItemTag.IsEmpty && item.HasTag(ItemTag))) { if (!ApplyTagToItem.IsEmpty) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs index dce9b2f62..dd3375657 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitForItemUsedAction.cs @@ -7,30 +7,33 @@ using System.Linq; namespace Barotrauma { + /// + /// Waits for some item(s) to be used before continuing the execution of the event. + /// class WaitForItemUsedAction : EventAction { - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the item that must be used. Note that the item needs to have been tagged by the event - this does not refer to the tags that can be set per-item in the sub editor.")] public Identifier ItemTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the character that must use the item. If there's multiple matching characters, it's enough if any of them use the item. If empty, it doesn't matter who uses the item.")] public Identifier UserTag { get; set; } - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("", IsPropertySaveable.Yes, description: "Name of the ItemComponent that the character must use. If empty, the character attempts to use all of them.")] public Identifier TargetItemComponent { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target item when it's used.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag to apply to the target item when it's used.")] public Identifier ApplyTagToItem { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the user when the target item is used.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag to apply to the user when the target item is used.")] public Identifier ApplyTagToUser{ get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag to apply to the hull the target item is inside when the item is used.")] public Identifier ApplyTagToHull { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")] + [Serialize("", IsPropertySaveable.Yes, description: "Optional tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")] public Identifier ApplyTagToLinkedHulls { get; set; } - [Serialize(1, IsPropertySaveable.Yes)] + [Serialize(1, IsPropertySaveable.Yes, description: "How many times does the item need to be used. Defaults to 1.")] public int RequiredUseCount { get; set; } private bool isFinished; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 72f17db3c..183777f8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -144,7 +144,7 @@ namespace Barotrauma public bool Enabled = true; private MTRandom random; - private int randomSeed; + public int RandomSeed { get; private set; } public void StartRound(Level level) { @@ -171,13 +171,13 @@ namespace Barotrauma if (level != null) { - randomSeed = ToolBox.StringToInt(level.Seed); + RandomSeed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - randomSeed ^= ToolBox.IdentifierToInt(previousEvent); + RandomSeed ^= ToolBox.IdentifierToInt(previousEvent); } } - random = new MTRandom(randomSeed); + random = new MTRandom(RandomSeed); bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; EventSet initialEventSet = SelectRandomEvents( @@ -214,7 +214,7 @@ namespace Barotrauma var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); if (unlockPathEventPrefab != null) { - var newEvent = unlockPathEventPrefab.CreateInstance(); + var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed); activeEvents.Add(newEvent); } else @@ -250,7 +250,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}."); continue; } - var ev = eventPrefab.CreateInstance(); + var ev = eventPrefab.CreateInstance(RandomSeed); if (ev != null) { QueuedEvents.Enqueue(ev); @@ -543,9 +543,8 @@ namespace Barotrauma if (eventPrefabs != null && random.NextDouble() <= probability) { var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(RandomSeed); if (newEvent == null) { continue; } - newEvent.RandomSeed = randomSeed; if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true); if (!selectedEvents.ContainsKey(eventSet)) @@ -588,8 +587,9 @@ namespace Barotrauma if (random.NextDouble() > probability) { continue; } var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random); - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(RandomSeed); if (newEvent == null) { continue; } + if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); @@ -730,11 +730,9 @@ namespace Barotrauma float distFromStart = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.StartExitPosition.ToPoint(), level.StartPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); float distFromEnd = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.EndExitPosition.ToPoint(), level.EndPosition.ToPoint(), refEntity.WorldPosition.ToPoint())); - //don't create new events if within 50 meters of the start/end of the level if (!eventSet.AllowAtStart) { - if (distanceTraveled <= 0.0f || - distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || + if (distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) { return false; @@ -767,7 +765,7 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!Enabled || level == null) { return; } + if (!Enabled) { return; } if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; } if (!eventsInitialized) @@ -871,6 +869,10 @@ namespace Barotrauma { pendingEventSets.Add(eventSet); CreateEvents(eventSet); + foreach (Event newEvent in selectedEvents[eventSet]) + { + if (!newEvent.Initialized) { newEvent.Init(eventSet); } + } }; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 721f1bcb0..f6a8847ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -99,19 +99,19 @@ namespace Barotrauma UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); } - public bool TryCreateInstance(out T instance) where T : Event + public bool TryCreateInstance(int seed, out T instance) where T : Event { - instance = CreateInstance() as T; + instance = CreateInstance(seed) as T; return instance is not null; } - public Event CreateInstance() + public Event CreateInstance(int seed) { - ConstructorInfo constructor = EventType.GetConstructor(new[] { GetType() }); + ConstructorInfo constructor = EventType.GetConstructor(new[] { GetType(), typeof(int) }); Event instance = null; try { - instance = constructor.Invoke(new object[] { this }) as Event; + instance = constructor.Invoke(new object[] { this, seed }) as Event; } catch (Exception ex) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index a21bc4cf2..6aed0f365 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -372,19 +372,19 @@ namespace Barotrauma MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); - AllowAtStart = element.GetAttributeBool("allowatstart", false); + AllowAtStart = element.GetAttributeBool("allowatstart", parentSet?.AllowAtStart ?? false); PerRuin = element.GetAttributeBool("perruin", false); PerCave = element.GetAttributeBool("percave", false); PerWreck = element.GetAttributeBool("perwreck", false); - DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); + DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", parentSet?.DisableInHuntingGrounds ?? false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); IgnoreIntensity = element.GetAttributeBool("ignoreintensity", parentSet?.IgnoreIntensity ?? false); - DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); - OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); - TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); + DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", parentSet?.DelayWhenCrewAway ?? (!PerRuin && !PerCave && !PerWreck)); + OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", parentSet?.OncePerLevel ?? false)); + TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", parentSet?.TriggerEventCooldown ?? true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0); - CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false); + CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false); ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); @@ -449,6 +449,11 @@ namespace Barotrauma EventPrefabs = eventPrefabs.ToImmutableArray(); ChildSets = childSets.ToImmutableArray(); OverrideCommonness = overrideCommonness.ToImmutableDictionary(); + + if ((PerRuin && PerCave) || (PerWreck && PerCave) || (PerRuin && PerWreck)) + { + DebugConsole.AddWarning($"Error in event set \"{Identifier}\". Only one of the settings {nameof(PerRuin)}, {nameof(PerCave)} or {nameof(PerWreck)} can be enabled at the time."); + } } public void CheckLocationTypeErrors() @@ -560,7 +565,8 @@ namespace Barotrauma static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab, Func filter = null) { - if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) + if (eventPrefab.EventType == typeof(MonsterEvent) && + eventPrefab.TryCreateInstance(GameMain.GameSession?.EventManager?.RandomSeed ?? 0, out MonsterEvent monsterEvent)) { if (filter != null && !filter(monsterEvent)) { return; } float spawnProbability = monsterEvent.Prefab?.Probability ?? 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs index 47c73a3eb..306a0f8a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs @@ -25,8 +25,8 @@ namespace Barotrauma return "MalfunctionEvent (" + string.Join(", ", targetItemIdentifiers) + ")"; } - public MalfunctionEvent(EventPrefab prefab) - : base(prefab) + public MalfunctionEvent(EventPrefab prefab, int seed) + : base(prefab, seed) { targetItems = new List(); @@ -39,9 +39,8 @@ namespace Barotrauma targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty()); } - public override void Init(EventSet parentSet) + protected override void InitEventSpecific(EventSet parentSet) { - base.Init(parentSet); var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)); int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient); for (int i = 0; i < itemAmount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 3f22e082b..81e579c80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -1,4 +1,4 @@ -using Barotrauma.Items.Components; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -137,18 +137,21 @@ namespace Barotrauma } } - var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced); - foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs) + if (monsterSets.Any()) { - int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); - for (int i = 0; i < amount; i++) + var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced); + foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs) { - CoroutineManager.Invoke(() => + int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); + for (int i = 0; i < amount; i++) { - //round ended before the coroutine finished - if (GameMain.GameSession == null || Level.Loaded == null) { return; } - Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos); - }, Rand.Range(0f, amount)); + CoroutineManager.Invoke(() => + { + //round ended before the coroutine finished + if (GameMain.GameSession == null || Level.Loaded == null) { return; } + Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos); + }, Rand.Range(0f, amount)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 2d40a2fc5..47b0092de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -15,6 +15,9 @@ namespace Barotrauma private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); + /// + /// Percentage of items (0.0 - 1.0) needed to be delivered to complete the mission. + /// private float requiredDeliveryAmount; private readonly List<(ContentXElement element, ItemContainer container)> itemsToSpawn = new List<(ContentXElement element, ItemContainer container)>(); @@ -86,7 +89,7 @@ namespace Barotrauma bool isPriorMission = true; foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { - if (!(mission is CargoMission otherMission)) { continue; } + if (mission is not CargoMission otherMission) { continue; } if (mission == this) { isPriorMission = false; } previouslySelectedMissions.Add(otherMission); if (!isPriorMission) { continue; } @@ -99,7 +102,8 @@ namespace Barotrauma { int maxCount = subElement.GetAttributeInt("maxcount", 10); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } - ItemPrefab itemPrefab = FindItemPrefab(subElement); + // For logging purposes + FindItemPrefab(subElement); while (itemsToSpawn.Count < maxItemCount) { itemsToSpawn.Add((subElement, null)); @@ -121,7 +125,7 @@ namespace Barotrauma bool isPriorMission = true; foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions) { - if (!(mission is CargoMission otherMission)) { continue; } + if (mission is not CargoMission otherMission) { continue; } if (mission == this) { isPriorMission = false; } previouslySelectedMissions.Add(otherMission); if (!isPriorMission) { continue; } @@ -161,27 +165,53 @@ namespace Barotrauma itemsToSpawn.Add((itemConfig.Elements().First(), null)); } + // Calculate the current total reward, since it might differ from the + // prefab total reward depending on the current actual crate count. calculatedReward = 0; + bool crateValuesUniform = true; + int? prevCrateReward = null; foreach (var (element, container) in itemsToSpawn) { - int price = element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count); - if (rewardPerCrate.HasValue) + int currentCrateReward = element.GetAttributeInt("reward", 0); + calculatedReward += currentCrateReward; + + // Apparently crates can have varying values, so we need to check + // here if that is the case, stopping checks on the first discrepancy + if (crateValuesUniform) { - if (price != rewardPerCrate.Value) { rewardPerCrate = -1; } + if (prevCrateReward.HasValue) + { + if (prevCrateReward.Value != currentCrateReward) + { + crateValuesUniform = false; + } + } + prevCrateReward = currentCrateReward; } - else - { - rewardPerCrate = price; - } - calculatedReward += price; } - if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; } + + if (crateValuesUniform) + { + // If rewardPerCrate is set, it will be displayed in the client UI as eg. "123 mk x 5" + rewardPerCrate = calculatedReward / itemsToSpawn.Count; + } + else + { + // If rewardPerCrate is null, the client UI will display just the total reward + rewardPerCrate = null; + } + + // Apply the mission reward campaign setting multiplier to the per-crate price, too + if (GameMain.GameSession?.Campaign is CampaignMode campaign && rewardPerCrate is int confirmedRewardPerCrate) + { + rewardPerCrate = (int)Math.Round(confirmedRewardPerCrate * campaign.Settings.MissionRewardMultiplier); + } string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(currentSub))}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } - public override int GetReward(Submarine sub) + public override int GetBaseReward(Submarine sub) { // If we are not at the location of the mission, skip the calculation of the reward if (GameMain.GameSession?.StartLocation != Locations[0]) @@ -272,7 +302,7 @@ namespace Barotrauma item.FindHull(); items.Add(item); - if (parent != null && parent.GetComponent() != null) + if (parent?.GetComponent() != null) { parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(parent.GetComponent())); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 9c900601f..39838a1d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -61,7 +61,7 @@ namespace Barotrauma if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } - public override int GetReward(Submarine sub) + public override int GetBaseReward(Submarine sub) { if (sub != missionSub) { @@ -160,14 +160,13 @@ namespace Barotrauma if (terroristChance > 0f) { - int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count); + int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count); terroristCount = Math.Clamp(terroristCount, 1, characters.Count); terroristCharacters.Clear(); characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c)); - + terroristCharacters.ForEach(c => c.IsHostileEscortee = true); terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f); - #if DEBUG DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared)); foreach (Character character in terroristCharacters) @@ -251,6 +250,7 @@ namespace Barotrauma // decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards foreach (Character character in terroristCharacters) { + character.IsHostileEscortee = true; if (character.HasTeamChange(TerroristTeamChangeIdentifier)) { // already triggered diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 561fed7d5..10d174170 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -259,7 +259,7 @@ namespace Barotrauma } else if (owner is Character c) { - return c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info); + return c.Info != null && GameMain.GameSession.CrewManager.GetCharacterInfos().Contains(c.Info); } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index be2130aac..060d47af6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -183,33 +183,32 @@ namespace Barotrauma completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})"); } - for (int n = 0; n < 2; n++) - { - string locationName = $"‖color:gui.orange‖{locations[n].DisplayName}‖end‖"; - if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); } - if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); } - if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); } - for (int m = 0; m < messages.Length; m++) - { - messages[m] = messages[m].Replace("[location" + (n + 1) + "]", locationName); - } - } - string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; - if (description != null) - { - descriptionWithoutReward = description; - description = description.Replace("[reward]", rewardText); - } - if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); } - if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); } + descriptionWithoutReward = ReplaceVariablesInMissionMessage(description, sub, replaceReward: false); + description = ReplaceVariablesInMissionMessage(description, sub); + successMessage = ReplaceVariablesInMissionMessage(successMessage, sub); + failureMessage = ReplaceVariablesInMissionMessage(failureMessage, sub); for (int m = 0; m < messages.Length; m++) { - messages[m] = messages[m].Replace("[reward]", rewardText); + messages[m] = ReplaceVariablesInMissionMessage(messages[m], sub); } - Messages = messages.ToImmutableArray(); } + public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true) + { + for (int locationIndex = 0; locationIndex < 2; locationIndex++) + { + string locationName = $"‖color:gui.orange‖{Locations[locationIndex].DisplayName}‖end‖"; + message = message.Replace("[location" + (locationIndex + 1) + "]", locationName); + } + if (replaceReward) + { + string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; + message = message.Replace("[reward]", rewardText); + } + return message; + } + public virtual void SetLevel(LevelData level) { } public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false) @@ -254,11 +253,30 @@ namespace Barotrauma return null; } - public virtual int GetReward(Submarine sub) + /// + /// Calculates the base reward, can be overridden for different mission types + /// + public virtual int GetBaseReward(Submarine sub) { return Prefab.Reward; } + /// + /// Calculates the available reward, taking into account universal modifiers such as campaign settings + /// + public int GetReward(Submarine sub) + { + int reward = GetBaseReward(sub); + + // Some modifiers should apply universally to all implementations of GetBaseReward + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + reward = (int)Math.Round(reward * campaign.Settings.MissionRewardMultiplier); + } + + return reward; + } + public void Start(Level level) { state = 0; @@ -353,7 +371,7 @@ namespace Barotrauma } if (GameMain.GameSession?.EventManager != null) { - var newEvent = eventPrefab.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed); GameMain.GameSession.EventManager.ActivateEvent(newEvent); } } @@ -455,9 +473,13 @@ namespace Barotrauma foreach (var reputationReward in ReputationRewards) { + var reputationGainMultiplier = new AbilityMissionReputationGainMultiplier(this, 1f, character: null); + foreach (var c in crewCharacters) { c.CheckTalents(AbilityEffectType.OnCrewGainMissionReputation, reputationGainMultiplier); } + float amount = reputationReward.Amount * reputationGainMultiplier.Value; + if (reputationReward.FactionIdentifier == "location") { - OriginLocation.Reputation?.AddReputation(reputationReward.Amount); + OriginLocation.Reputation?.AddReputation(amount); TryGiveReputationForOpposingFaction(OriginLocation.Faction, reputationReward.AmountForOpposingFaction); } else @@ -465,7 +487,7 @@ namespace Barotrauma Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.FactionIdentifier); if (faction != null) { - faction.Reputation.AddReputation(reputationReward.Amount); + faction.Reputation.AddReputation(amount); TryGiveReputationForOpposingFaction(faction, reputationReward.AmountForOpposingFaction); } } @@ -664,5 +686,19 @@ namespace Barotrauma public Mission Mission { get; set; } public Character Character { get; set; } } + + class AbilityMissionReputationGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter + { + public AbilityMissionReputationGainMultiplier(Mission mission, float reputationMultiplier, Character character) + { + Value = reputationMultiplier; + Mission = mission; + Character = character; + } + + public float Value { get; set; } + public Mission Mission { get; set; } + public Character Character { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 064527a38..96454bb6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -110,6 +110,7 @@ namespace Barotrauma public readonly int Reward; + // The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters. public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; @@ -187,23 +188,25 @@ namespace Barotrauma Tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); - string nameTag = element.GetAttributeString("name", ""); - Name = TextManager.Get($"MissionName.{TextIdentifier}"); - if (!string.IsNullOrEmpty(nameTag)) - { - Name = Name - .Fallback(TextManager.Get(nameTag)) - .Fallback(nameTag); - } + Name = GetText(element.GetAttributeString("name", ""), "MissionName"); + Description = GetText(element.GetAttributeString("description", ""), "MissionDescription"); - string descriptionTag = element.GetAttributeString("description", ""); - Description = - TextManager.Get($"MissionDescription.{TextIdentifier}"); - if (!string.IsNullOrEmpty(descriptionTag)) + LocalizedString GetText(string textTag, string textTagPrefix) { - Description = Description - .Fallback(TextManager.Get(descriptionTag)) - .Fallback(descriptionTag); + if (string.IsNullOrEmpty(textTag)) + { + return TextManager.Get($"{textTagPrefix}.{TextIdentifier}"); + } + else + { + return + //prefer finding a text based on the specific text tag defined in the mission config + TextManager.Get(textTag) + //2nd option: the "default" format (MissionName.SomeMission) + .Fallback(TextManager.Get($"{textTagPrefix}.{TextIdentifier}")) + //last option: use the text in the xml as-is with no localization + .Fallback(textTag); + } } Reward = element.GetAttributeInt("reward", 1); @@ -372,6 +375,12 @@ namespace Barotrauma DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); return; } +#if DEBUG + if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty()) + { + DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set."); + } +#endif if (CoOpMissionClasses.ContainsKey(Type)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index e87e70696..1819a821b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -68,7 +68,7 @@ namespace Barotrauma } } - public override int GetReward(Submarine sub) + public override int GetBaseReward(Submarine sub) { return alternateReward; } @@ -262,13 +262,13 @@ namespace Barotrauma enemySub.EnableMaintainPosition(); enemySub.TeamID = CharacterTeamType.None; //make the enemy sub withstand atleast the same depth as the player sub - enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth); + enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth)); if (Level.Loaded != null) { //...and the depth of the patrol positions + 1000 m foreach (var patrolPos in patrolPositions) { - enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000); + enemySub.SetCrushDepth(Math.Max(enemySub.RealWorldCrushDepth, Level.Loaded.GetRealWorldDepth(patrolPos.Y) + 1000)); } } enemySub.ImmuneToBallastFlora = true; @@ -394,11 +394,11 @@ namespace Barotrauma DebugConsole.NewMessage("Patrol pos: " + patrolPos); } #endif + enemySub.SetPosition(spawnPos); if (!IsClient) { InitPirateShip(); } - enemySub.SetPosition(spawnPos); // flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine // creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index c8184730c..8b3059af8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -10,10 +10,13 @@ namespace Barotrauma { partial class SalvageMission : Mission { - private class Target { public Item Item; + /// + /// The target this item spawns inside (usually a crate for example). + /// + public Target ParentTarget; /// /// Note that the integer values matter here: @@ -29,14 +32,20 @@ namespace Barotrauma } public readonly ItemPrefab ItemPrefab; + /// + /// Where the target can be spawned to. E.g. MainPath or Wreck. + /// public readonly Level.PositionType SpawnPositionType; public readonly Identifier ContainerTag; public readonly Identifier ExistingItemTag; - + public readonly bool RemoveItem; public readonly LocalizedString SonarLabel; + /// + /// Can the mission continue before this target has been retrieved? Can be used if you want the targets to be retrieved in a specific order. + /// public readonly bool AllowContinueBeforeRetrieved; /// @@ -51,6 +60,13 @@ namespace Barotrauma { get { + //if placing the item inside the parent (e.g. some item inside a crate) failed, + //consider this item retrieved (= essentially ignoring the item, it's not necessary to retrieve) + if (PlacingInsideParentTargetFailed) + { + return true; + } + return RequiredRetrievalState switch { RetrievalState.None => true, @@ -78,20 +94,29 @@ namespace Barotrauma public bool Interacted; private readonly SalvageMission mission; + public readonly bool RequireInsideOriginalContainer; + public Item OriginalContainer; + + /// + /// Means that the item could not be placed inside the container it was intended to spawn inside (probably meaning the mission has been misconfigured to e.g. spawn more items inside a crate than what the crate can hold). + /// + public bool PlacingInsideParentTargetFailed; /// /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. /// public readonly List> StatusEffects = new List>(); - public Target(ContentXElement element, SalvageMission mission) + public Target(ContentXElement element, SalvageMission mission, Target parentTarget) { this.mission = mission; + ParentTarget = parentTarget; ContainerTag = element.GetAttributeIdentifier("containertag", Identifier.Empty); - RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub); - AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false); - HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false); - + RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", parentTarget?.RequiredRetrievalState ?? RetrievalState.RetrievedToSub); + AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", parentTarget != null); + HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", parentTarget?.HideLabelAfterRetrieved ?? false); + RequireInsideOriginalContainer = element.GetAttributeBool("requireinsideoriginalcontainer", false); + string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); if (!string.IsNullOrEmpty(sonarLabelTag)) { @@ -126,6 +151,7 @@ namespace Barotrauma if (ItemPrefab == null) { string itemTag = element.GetAttributeString("itemtag", ""); + //NOTE: using unsynced random here is fine, the clients receive the info of what item spawned from the server ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; } if (ItemPrefab == null && ExistingItemTag.IsEmpty) @@ -135,7 +161,7 @@ namespace Barotrauma } } - SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin); + SpawnPositionType = element.GetAttributeEnum("spawntype", parentTarget?.SpawnPositionType ?? (Level.PositionType.Cave | Level.PositionType.Ruin)); foreach (var subElement in element.Elements()) { @@ -149,12 +175,15 @@ namespace Barotrauma break; } case "chooserandom": - StatusEffects.Add(new List()); - foreach (var effectElement in subElement.Elements()) + if (subElement.Elements().Any(static e => e.NameAsIdentifier() == "statuseffect")) { - var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value); - if (newEffect == null) { continue; } - StatusEffects.Last().Add(newEffect); + StatusEffects.Add(new List()); + foreach (var effectElement in subElement.Elements()) + { + var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Last().Add(newEffect); + } } break; } @@ -170,7 +199,24 @@ namespace Barotrauma private readonly List targets = new List(); - public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved); + /// + /// What percentage of targets need to be retrieved for the mission to complete (0.0 - 1.0). Defaults to 0.98. + /// + private readonly float requiredDeliveryAmount; + + /// + /// Message displayed when at least one of the targets is retrieved, but the mission is not complete yet. + /// + private LocalizedString partiallyRetrievedMessage; + + /// + /// Message displayed when all targets have been retrieved. + /// + private LocalizedString allRetrievedMessage; + + public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(static t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved); + + private readonly MTRandom rng; public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { @@ -179,8 +225,23 @@ namespace Barotrauma foreach (var target in targets) { if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; } - if (target.Item != null) + if (target.Item != null && !target.Item.Removed) { + if (target.Item.ParentInventory?.Owner is Item parentItem) + { + bool insideParentItem = false; + foreach (var parentTarget in targets) + { + if (parentTarget.Item == parentItem && !parentTarget.SonarLabel.IsNullOrEmpty()) + { + insideParentItem = true; + break; + } + } + //if the item is inside another target that has it's own sonar label, no need to show one on this item + if (insideParentItem) { continue; } + } + yield return ( target.SonarLabel ?? Prefab.SonarLabel, target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition); @@ -193,17 +254,82 @@ namespace Barotrauma public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { + requiredDeliveryAmount = prefab.ConfigElement.GetAttributeFloat(nameof(requiredDeliveryAmount), 0.98f); + + //LevelData may not be instantiated at this point, in that case use the name identifier of the location + rng = new MTRandom(ToolBox.StringToInt( + locations[0].LevelData?.Seed ?? locations[0].NameIdentifier.Value + + locations[1].LevelData?.Seed ?? locations[1].NameIdentifier.Value)); + + partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage)); + allRetrievedMessage = GetMessage(nameof(allRetrievedMessage)); + foreach (ContentXElement subElement in prefab.ConfigElement.Elements()) { - if (subElement.NameAsIdentifier() == "target") + if (subElement.NameAsIdentifier() == "target" || + subElement.NameAsIdentifier() == "chooserandom") { - targets.Add(new Target(subElement, this)); + LoadTarget(subElement, parentTarget: null); } + } if (!targets.Any()) { - targets.Add(new Target(prefab.ConfigElement, this)); + targets.Add(new Target(prefab.ConfigElement, this, parentTarget: null)); } + + LocalizedString GetMessage(string attributeName) + { + if (prefab.ConfigElement.GetAttribute(attributeName) != null) + { + string msgTag = prefab.ConfigElement.GetAttributeString(attributeName, string.Empty); + return ReplaceVariablesInMissionMessage(TextManager.Get(msgTag).Fallback(msgTag), sub); + } + return string.Empty; + } + } + + private void LoadTarget(ContentXElement element, Target parentTarget) + { + ContentXElement chosenElement = element; + if (element.NameAsIdentifier() == "chooserandom") + { + /* chooserandom in this context can be used to choose either between targets or status effects to apply to the target, + ensure we don't try to load a statuseffect as a "child target" */ + if (element.Elements().Any(static e => e.NameAsIdentifier() == "statuseffect")) + { + return; + } + //this needs to be deterministic, use RNG with a specific seed + chosenElement = element.Elements().ToList().GetRandom(rng); + } + + int amount = GetAmount(chosenElement); + for (int i = 0; i < amount; i++) + { + var target = new Target(chosenElement, this, parentTarget); + targets.Add(target); + foreach (ContentXElement subElement in chosenElement.Elements()) + { + LoadTarget(subElement, parentTarget: target); + } + } + } + + private int GetAmount(ContentXElement targetElement) + { + int amount = targetElement.GetAttributeInt("amount", 1); + int minAmount = targetElement.GetAttributeInt("minamount", amount); + int maxAmount = targetElement.GetAttributeInt("maxamount", amount); + + // if the amount is a range, pick a random value between minAmount and maxAmount + if (minAmount < maxAmount) + { + //this needs to be deterministic, use RNG with a specific seed + amount = rng.Next(minAmount, maxAmount + 1); + } + + return amount; } protected override void StartMissionSpecific(Level level) @@ -294,8 +420,16 @@ namespace Barotrauma continue; } target.Item = new Item(target.ItemPrefab, position, null); - target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation); - target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; +#if CLIENT + target.Item.HighlightColor = GUIStyle.Orange; + target.Item.ExternalHighlight = true; +#endif + target.Item.UpdateTransform(); + if (target.Item.CurrentHull == null) + { + //prevent the body from moving if it spawned outside the hulls (we don't want it e.g. falling to the bottom of a cave or into the abyss) + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; + } } else if (target.RequiredRetrievalState == Target.RetrievalState.Interact) { @@ -344,6 +478,7 @@ namespace Barotrauma } if (validContainers.Any()) { + //NOTE: using unsynced random here is fine, clients don't run this logic but rely on where the server places the item var selectedContainer = validContainers.GetRandomUnsynced(); if (selectedContainer.Combine(target.Item, user: null)) { @@ -362,6 +497,40 @@ namespace Barotrauma new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices)); #endif } + + if (!IsClient) + { + // after spawning all the items from prefabs, need to find all targets where parentTarget is defined, and set the item inside parent target container (if applicable) + foreach (var target in targets) + { + if (target.ParentTarget == null) { continue; } + + if (target.Item == null) + { + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)", + contentPackage: Prefab.ContentPackage); + continue; + } + + if (target.ParentTarget.Item == null) + { + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (parent item was null)", + contentPackage: Prefab.ContentPackage); + continue; + } + + if (target.ParentTarget.Item.GetComponent() is ItemContainer container) + { + if (!container.Inventory.TryPutItem(target.Item, user: null)) + { + DebugConsole.ThrowError($"Error in salvage mission {Prefab.Identifier}: failed to put the item {target.Item.Name} inside {target.ParentTarget.Item.Name}.", + contentPackage: Prefab.ContentPackage); + target.PlacingInsideParentTargetFailed = true; + } + target.OriginalContainer = target.ParentTarget.Item; + } + } + } } protected override void UpdateMissionSpecific(float deltaTime) @@ -376,6 +545,7 @@ namespace Barotrauma if (IsClient) { return; } + bool atLeastOneTargetWasRetrieved = false; for (int i = 0; i < targets.Count; i++) { var target = targets[i]; @@ -388,6 +558,10 @@ namespace Barotrauma #endif return; } + + Entity rootInventoryOwner = target.Item.GetRootInventoryOwner(); + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine; + bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player; switch (target.State) { case Target.RetrievalState.None: @@ -401,16 +575,16 @@ namespace Barotrauma { TrySetRetrievalState(Target.RetrievalState.PickedUp); } + if (inPlayerSub) + { + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + } } - break; case Target.RetrievalState.PickedUp: case Target.RetrievalState.RetrievedToSub: { - Entity rootInventoryOwner = target.Item.GetRootInventoryOwner(); - Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine; - bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player; bool inPlayerInventory = false; bool playerInFriendlySub = false; if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1) @@ -441,33 +615,70 @@ namespace Barotrauma if (retrievalState < target.State || target.State == retrievalState) { return; } bool wasRetrieved = target.Retrieved; target.State = retrievalState; - //increment the mission state if the target became retrieved - if (!wasRetrieved && target.Retrieved) { State = i + 1; } + //increment the mission state if the target became retrieved + if (!wasRetrieved && target.Retrieved) + { + State = Math.Max(i + 1, State); + atLeastOneTargetWasRetrieved = true; + } } } +#if CLIENT + if (atLeastOneTargetWasRetrieved) + { + TryShowRetrievedMessage(); + } +#endif if (targets.All(t => t.Retrieved)) { State = targets.Count + 1; - } + } } protected override bool DetermineCompleted() { - return targets.All(t => t.State >= t.RequiredRetrievalState); + if (requiredDeliveryAmount < 1.0f) + { + return targets.Count(t => IsTargetRetrieved(t)) / (float)targets.Count >= requiredDeliveryAmount; + } + else + { + return targets.All(IsTargetRetrieved); + } + + static bool IsTargetRetrieved(Target target) + { + if (target.State < target.RequiredRetrievalState) { return false; } + if (target.RequireInsideOriginalContainer) + { + if (target.Item.ParentInventory != target.OriginalContainer?.OwnInventory) { return false; } + } + return true; + } } protected override void EndMissionSpecific(bool completed) { //consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp); + List targetsToRemove = new List(); foreach (var target in targets) { - if (target.RemoveItem) + if (target.RemoveItem || + /*remove the target if it's inside another target that's set to be removed (e.g. inside the crate it spawned in)*/ + targets.Any(t => t.RemoveItem && target.Item?.ParentInventory?.Owner as Item == t.Item)) { - target.Item?.Remove(); - target.Reset(); + targetsToRemove.Add(target); } } + foreach (var target in targetsToRemove) + { + if (target.Item != null && !target.Item.Removed) + { + target.Item.Remove(); + } + target.Reset(); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 187c8f919..e2c58bb74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -99,8 +99,8 @@ namespace Barotrauma } } - public MonsterEvent(EventPrefab prefab) - : base(prefab) + public MonsterEvent(EventPrefab prefab, int seed) + : base(prefab, seed) { string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", ""); CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile); @@ -173,9 +173,8 @@ namespace Barotrauma } } - public override void Init(EventSet parentSet) + protected override void InitEventSpecific(EventSet parentSet) { - base.Init(parentSet); if (parentSet != null && resetTime == 0) { // Use the parent reset time only if there's no reset time defined for the event. @@ -192,7 +191,7 @@ namespace Barotrauma int amount = Rand.Range(MinAmount, MaxAmount + 1); for (int i = 0; i < amount; i++) { - string seed = Level.Loaded.Seed + i.ToString(); + string seed = i.ToString() + Level.Loaded.Seed; Character createdCharacter = Character.Create(SpeciesName, Vector2.Zero, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true, throwErrorIfNotFound: false); if (createdCharacter == null) { @@ -271,14 +270,18 @@ namespace Barotrauma spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); - bool isRuinOrWreck = SpawnPosType.HasFlag(Level.PositionType.Ruin) || SpawnPosType.HasFlag(Level.PositionType.Wreck); - if (affectSubImmediately && !isRuinOrWreck && !SpawnPosType.HasFlag(Level.PositionType.Abyss)) + bool isRuinOrWreckOrCave = + SpawnPosType.HasFlag(Level.PositionType.Ruin) || + SpawnPosType.HasFlag(Level.PositionType.Wreck) || + SpawnPosType.HasFlag(Level.PositionType.Cave) || + SpawnPosType.HasFlag(Level.PositionType.AbyssCave); + if (affectSubImmediately && !isRuinOrWreckOrCave && !SpawnPosType.HasFlag(Level.PositionType.Abyss)) { if (availablePositions.None()) { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } Submarine refSub = GetReferenceSub(); @@ -348,7 +351,7 @@ namespace Barotrauma } else { - if (!isRuinOrWreck) + if (!isRuinOrWreckOrCave) { float minDistance = 20000; for (int i = 0; i < Submarine.MainSubs.Length; i++) @@ -361,7 +364,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } chosenPosition = availablePositions.GetRandomUnsynced(); @@ -371,21 +374,17 @@ namespace Barotrauma spawnPos = chosenPosition.Position.ToVector2(); if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) { - bool ignoreSubmarine = chosenPosition.Ruin != null; - var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag, ignoreSubmarine: ignoreSubmarine); + var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag); if (spawnPoint != null) { - if (!ignoreSubmarine) - { - System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine); - } + System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine)); spawnPos = spawnPoint.WorldPosition; } else { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } } @@ -422,7 +421,7 @@ namespace Barotrauma { //no suitable position found, disable the event spawnPos = null; - Finish(); + disallowed = true; return; } } @@ -442,11 +441,7 @@ namespace Barotrauma public override void Update(float deltaTime) { - if (disallowed) - { - Finish(); - return; - } + if (disallowed) { return; } if (resetTimer > 0) { @@ -483,8 +478,8 @@ namespace Barotrauma } FindSpawnPosition(affectSubImmediately: true); - //the event gets marked as finished if a spawn point is not found - if (isFinished) { return; } + //the event gets marked as disallowed if a spawn point is not found + if (isFinished || disallowed) { return; } spawnPending = true; } @@ -493,7 +488,7 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(spawnPos.HasValue); if (spawnPos == null) { - Finish(); + disallowed = true; return; } //wait until there are no submarines at the spawnpos @@ -567,7 +562,7 @@ namespace Barotrauma if (CheckLineOfSight(from, to, chosenPosition.Submarine)) { // Line of sight to a player character -> don't spawn. Disable the event to prevent monsters "magically" spawning here. - Finish(); + disallowed = true; return; } } @@ -636,7 +631,7 @@ namespace Barotrauma scatterAmount = scatter; } } - else if (!SpawnPosType.HasFlag(Level.PositionType.MainPath)) + else if (SpawnPosType.IsIndoorsArea()) { scatterAmount = 0; } @@ -650,22 +645,46 @@ namespace Barotrauma if (GameMain.GameSession == null || Level.Loaded == null) { return; } if (monster.Removed) { return; } - + System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events."); - Vector2 pos = spawnPos.Value + Rand.Vector(scatterAmount); + Vector2 pos = spawnPos.Value; if (scatterAmount > 0) { - if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos))) + //try finding an offset position that's not inside a wall + int tries = 10; + do { - // Can't use the offset position, let's use the exact spawn position. - pos = spawnPos.Value; - } - else if (Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos))) - { - // Can't use the offset position, let's use the exact spawn position. - pos = spawnPos.Value; - } + tries--; + pos = spawnPos.Value + Rand.Vector(Rand.Range(0.0f, scatterAmount)); + + bool isValidPos = true; + if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos)) || + Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos)) || + Level.Loaded.IsPositionInsideWall(pos)) + { + isValidPos = false; + } + else if (SpawnPosType.HasFlag(Level.PositionType.Cave) || SpawnPosType.HasFlag(Level.PositionType.AbyssCave)) + { + //trying to spawn in a cave, but the position is not inside a cave -> not valid + if (Level.Loaded.Caves.None(c => c.Area.Contains(pos))) + { + isValidPos = false; + } + } + + if (isValidPos) + { + //not inside anything, all good! + break; + } + // This was the last try and couldn't find an offset position, let's use the exact spawn position. + if (tries == 0) + { + pos = spawnPos.Value; + } + } while (tries > 0); } monster.Enabled = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 517875dd4..68218dc1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -51,7 +51,7 @@ namespace Barotrauma return $"{nameof(ScriptedEvent)} ({prefab.Identifier})"; } - public ScriptedEvent(EventPrefab prefab) : base(prefab) + public ScriptedEvent(EventPrefab prefab, int seed) : base(prefab, seed) { foreach (var element in prefab.ConfigElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 794567a60..29d81df7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -35,12 +35,7 @@ namespace Barotrauma //spawn items in wrecks, beacon stations and pirate subs foreach (var sub in Submarine.Loaded) { - if (sub.Info.Type == SubmarineType.Player || - sub.Info.Type == SubmarineType.Outpost || - sub.Info.Type == SubmarineType.OutpostModule) - { - continue; - } + if (sub.Info.Type is SubmarineType.Player or SubmarineType.Outpost or SubmarineType.OutpostModule) { continue; } if (sub.Info.InitialSuppliesSpawned) { continue; } CreateAndPlace(sub.ToEnumerable()); sub.Info.InitialSuppliesSpawned = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 9f6cbf6dc..139ae6c5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -661,6 +661,10 @@ namespace Barotrauma #if SERVER Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif + if (item.GetComponent() is { Attached: true }) + { + item.Drop(dropper: null); + } if (!character.Inventory.TryPutItem(item, user: null, item.AllowedSlots)) { foreach (Item containedItem in character.Inventory.AllItemsMod) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 762807d0d..2b5183312 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -35,8 +35,6 @@ namespace Barotrauma private Character welcomeMessageNPC; - public List CharacterInfos => characterInfos; - public bool HasBots { get; set; } public class ActiveOrder @@ -74,8 +72,8 @@ namespace Barotrauma } // Ignore orders work a bit differently since the "unignore" order counters the "ignore" order - var isUnignoreOrder = order.Identifier == "unignorethis"; - var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs["ignorethis"]; + var isUnignoreOrder = order.Identifier == Tags.UnignoreThis; + var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs[Tags.IgnoreThis]; ActiveOrder existingOrder = ActiveOrders.Find(o => o.Order.Prefab == orderPrefab && MatchesTarget(o.Order.TargetEntity, order.TargetEntity) && (o.Order.TargetType != Order.OrderTargetType.WallSection || o.Order.WallSectionIndex == order.WallSectionIndex)); @@ -95,6 +93,23 @@ namespace Barotrauma } else if (!isUnignoreOrder) { + if (order.IsDeconstructOrder) + { + if (order.TargetEntity is Item item) + { + if (order.Identifier == Tags.DeconstructThis) + { + Item.DeconstructItems.Add(item); +#if CLIENT + HintManager.OnItemMarkedForDeconstruction(order.OrderGiver); +#endif + } + else + { + Item.DeconstructItems.Remove(item); + } + } + } ActiveOrders.Add(new ActiveOrder(order, fadeOutTime)); #if CLIENT HintManager.OnActiveOrderAdded(order); @@ -198,6 +213,11 @@ namespace Barotrauma } } + public bool IsFired(Character character) + { + return !GetCharacterInfos().Contains(character.Info); + } + /// /// Remove the character from the crew (and crew menus). /// @@ -237,6 +257,11 @@ namespace Barotrauma characterInfos.Add(characterInfo); } + public void ClearCharacterInfos() + { + characterInfos.Clear(); + } + public void InitRound() { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 374768a3b..d66c8c079 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -147,6 +147,8 @@ namespace Barotrauma public bool DivingSuitWarningShown; + public bool ItemsRelocatedToMainSub; + private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) { if (GameMain.NetworkMember == null) { return true; } @@ -878,7 +880,7 @@ namespace Barotrauma } } - foreach (CharacterInfo ci in CrewManager.CharacterInfos.ToList()) + foreach (CharacterInfo ci in CrewManager.GetCharacterInfos().ToList()) { if (ci.CauseOfDeath != null) { @@ -979,7 +981,7 @@ namespace Barotrauma Preset?.Identifier.Value ?? "none"); string eventId = "FinishCampaign:"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0)); GameAnalyticsManager.AddDesignEvent(eventId + "Money", Bank.Balance); GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime); GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels); @@ -1024,7 +1026,7 @@ namespace Barotrauma return ToolBox.SelectWeightedRandom(factionsList, weights, random); } - public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Character hirer, Client client = null) + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney = true, Client client = null) { if (characterInfo == null) { return false; } if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) @@ -1034,8 +1036,8 @@ namespace Barotrauma return false; } } + if (takeMoney && !TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; } - if (!TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; } characterInfo.IsNewHire = true; characterInfo.Title = null; location.RemoveHireableCharacter(characterInfo); @@ -1047,7 +1049,6 @@ namespace Barotrauma private void NPCInteract(Character npc, Character interactor) { if (!npc.AllowCustomInteract) { return; } - GameAnalyticsManager.AddDesignEvent("CampaignInteraction:" + Preset.Identifier + ":" + npc.CampaignInteractionType); NPCInteractProjSpecific(npc, interactor); string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID); if (!CoroutineManager.IsCoroutineRunning(coroutineName)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs index fd5297d89..1ab312380 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignModePresets.cs @@ -8,7 +8,7 @@ namespace Barotrauma internal static class CampaignModePresets { public static readonly ImmutableArray List; - public static readonly ImmutableDictionary Definitions; + private static readonly ImmutableDictionary definitions; private static readonly string fileListPath = Path.Combine("Data", "campaignsettings.xml"); @@ -20,73 +20,58 @@ namespace Barotrauma return; } - List list = new List(); - Dictionary definitions = new Dictionary(); + List presetList = new List(); + Dictionary tempDefinitions = new Dictionary(); foreach (XElement element in docRoot.Elements()) { Identifier name = element.NameAsIdentifier(); + // The campaign setting presets if (name == CampaignSettings.LowerCaseSaveElementName) { - list.Add(new CampaignSettings(element)); + presetList.Add(new CampaignSettings(element)); } + // All the definitions for the setting value options else if (name == nameof(CampaignSettingDefinitions)) { + // The single definitions that the settings may refer to (eg. PatdownProbabilityMin) foreach (XElement subElement in element.Elements()) { - definitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement)); + tempDefinitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement)); } } } - List = list.ToImmutableArray(); - Definitions = definitions.ToImmutableDictionary(); + List = presetList.ToImmutableArray(); + definitions = tempDefinitions.ToImmutableDictionary(); + } + + public static bool TryGetAttribute(Identifier propertyName, Identifier attributeName, out XAttribute attribute) + { + attribute = null; + if (definitions.TryGetValue(propertyName, out CampaignSettingDefinitions definition)) + { + if (definition.Attributes.TryGetValue(attributeName, out XAttribute att)) + { + attribute = att; + return true; + } + } + return false; } } internal readonly struct CampaignSettingDefinitions { - // Definitely not the best way to do this - private readonly ImmutableDictionary> values; + public readonly ImmutableDictionary Attributes; public CampaignSettingDefinitions(XElement element) { - var definitions = new Dictionary>(); - foreach (XAttribute attribute in element.Attributes()) - { - Identifier name = attribute.NameAsIdentifier(); - if (attribute.Value.Contains('.')) - { - definitions.Add(name, element.GetAttributeFloat(name.Value, 0)); - } - else - { - definitions.Add(name, element.GetAttributeInt(name.Value, 0)); - } - } - - values = definitions.ToImmutableDictionary(); - } - - public float GetFloat(Identifier identifier) - { - float range = 0; - if (!values.TryGetValue(identifier, out Either value) || !value.TryGet(out range)) - { - DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}"); - } - return range; - } - - public int GetInt(Identifier identifier) - { - int integer = 0; - if (!values.TryGetValue(identifier, out Either value) || !value.TryGet(out integer)) - { - DebugConsole.ThrowError($"CampaignSettings: Can't find value for {identifier}"); - } - return integer; + Attributes = element.Attributes().ToImmutableDictionary( + a => a.NameAsIdentifier(), + a => a + ); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index a9f757191..ffafbc542 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Microsoft.Xna.Framework; using System.Collections.Generic; @@ -27,6 +27,10 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] public bool RadiationEnabled { get; set; } + public const int DefaultMaxMissionCount = 2; + public const int MaxMissionCountLimit = 10; + public const int MinMissionCountLimit = 1; + private int maxMissionCount; [Serialize(DefaultMaxMissionCount, IsPropertySaveable.Yes), NetworkSerialize(MinValueInt = MinMissionCountLimit, MaxValueInt = MaxMissionCountLimit)] @@ -38,60 +42,200 @@ namespace Barotrauma public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); - [Serialize(StartingBalanceAmount.Medium, IsPropertySaveable.Yes), NetworkSerialize] - public StartingBalanceAmount StartingBalanceAmount { get; set; } - - [Serialize(GameDifficulty.Medium, IsPropertySaveable.Yes), NetworkSerialize] - public GameDifficulty Difficulty { get; set; } + [Serialize(WorldHostilityOption.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public WorldHostilityOption WorldHostility { get; set; } [Serialize("normal", IsPropertySaveable.Yes), NetworkSerialize] public Identifier StartItemSet { get; set; } + [Serialize(StartingBalanceAmountOption.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public StartingBalanceAmountOption StartingBalanceAmount { get; set; } + + private int? _initialMoney; + public const int DefaultInitialMoney = 8000; + public int InitialMoney { get { - if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition)) + if (_initialMoney is int alreadyCachedValue) { - return definition.GetInt(StartingBalanceAmount.ToIdentifier()); + return alreadyCachedValue; + } + else + { + _initialMoney = DefaultInitialMoney; + Identifier settingDefinitionIdentifier = nameof(StartingBalanceAmount).ToIdentifier(); + Identifier attributeIdentifier = StartingBalanceAmount.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _initialMoney = attribute.GetAttributeInt(DefaultInitialMoney); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _initialMoney ?? DefaultInitialMoney; } - return 8000; } } + private float? _extraEventManagerDifficulty; + private const float defaultExtraEventManagerDifficulty = 0; + public float ExtraEventManagerDifficulty { get { - if (CampaignModePresets.Definitions.TryGetValue(nameof(ExtraEventManagerDifficulty).ToIdentifier(), out var definition)) + if (_extraEventManagerDifficulty is float alreadyCachedValue) { - return definition.GetFloat(Difficulty.ToIdentifier()); + return alreadyCachedValue; + } + else + { + _extraEventManagerDifficulty = defaultExtraEventManagerDifficulty; + Identifier settingDefinitionIdentifier = nameof(ExtraEventManagerDifficulty).ToIdentifier(); + Identifier attributeIdentifier = WorldHostility.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _extraEventManagerDifficulty = attribute.GetAttributeFloat(defaultExtraEventManagerDifficulty); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _extraEventManagerDifficulty ?? defaultExtraEventManagerDifficulty; } - return 0; } } + private float? _levelDifficultyMultiplier; + private const float defaultLevelDifficultyMultiplier = 1.0f; + public float LevelDifficultyMultiplier { get { - if (CampaignModePresets.Definitions.TryGetValue(nameof(LevelDifficultyMultiplier).ToIdentifier(), out var definition)) + if (_levelDifficultyMultiplier is float alreadyCachedValue) { - return definition.GetFloat(Difficulty.ToIdentifier()); + return alreadyCachedValue; + } + else + { + _levelDifficultyMultiplier = defaultLevelDifficultyMultiplier; + Identifier settingDefinitionIdentifier = nameof(LevelDifficultyMultiplier).ToIdentifier(); + Identifier attributeIdentifier = WorldHostility.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _levelDifficultyMultiplier = attribute.GetAttributeFloat(defaultLevelDifficultyMultiplier); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _levelDifficultyMultiplier ?? defaultLevelDifficultyMultiplier; } - return 1.0f; } } - [Serialize(0.2f, IsPropertySaveable.Yes, description: "How likely it is for security to inspect player characters for stolen items when your reputation is high?")] - public float MinStolenItemInspectionProbability { get; set; } + private static readonly Dictionary _multiplierSettings = new Dictionary + { + { "default", new MultiplierSettings { Min = 0.2f, Max = 2.0f, Step = 0.1f } }, + { nameof(CrewVitalityMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, + { nameof(NonCrewVitalityMultiplier),new MultiplierSettings { Min = 0.5f, Max = 3.0f, Step = 0.1f } }, + { nameof(MissionRewardMultiplier), new MultiplierSettings { Min = 0.5f, Max = 2.0f, Step = 0.1f } }, + { nameof(RepairFailMultiplier), new MultiplierSettings { Min = 0.5f, Max = 5.0f, Step = 0.5f } }, + { nameof(ShopPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } }, + { nameof(ShipyardPriceMultiplier), new MultiplierSettings { Min = 0.1f, Max = 3.0f, Step = 0.1f } } + // Add overrides for default values here + }; - [Serialize(0.9f, IsPropertySaveable.Yes, description: "How likely it is for security to inspect player characters for stolen items when your reputation is low?")] - public float MaxStolenItemInspectionProbability { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float CrewVitalityMultiplier { get; set; } - public const int DefaultMaxMissionCount = 2; - public const int MaxMissionCountLimit = 10; - public const int MinMissionCountLimit = 1; + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float NonCrewVitalityMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float OxygenMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float FuelMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float MissionRewardMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float ShopPriceMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float ShipyardPriceMultiplier { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes), NetworkSerialize] + public float RepairFailMultiplier { get; set; } + + [Serialize(true, IsPropertySaveable.Yes), NetworkSerialize] + public bool ShowHuskWarning { get; set; } + + [Serialize(PatdownProbabilityOption.Medium, IsPropertySaveable.Yes), NetworkSerialize] + public PatdownProbabilityOption PatdownProbability { get; set; } + + private float? _minPatdownProbability; + private float? _maxPatdownProbability; + public const float DefaultMinPatdownProbability = 0.2f; + public const float DefaultMaxPatdownProbability = 0.9f; + + public float PatdownProbabilityMin + { + get + { + if (_minPatdownProbability is float alreadyCachedValue) + { + return alreadyCachedValue; + } + else + { + _minPatdownProbability = DefaultMinPatdownProbability; + Identifier settingDefinitionIdentifier = nameof(PatdownProbabilityMin).ToIdentifier(); + Identifier attributeIdentifier = PatdownProbability.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _minPatdownProbability = attribute.GetAttributeFloat(DefaultMinPatdownProbability); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _minPatdownProbability ?? DefaultMinPatdownProbability; + } + } + } + + public float PatdownProbabilityMax + { + get + { + if (_maxPatdownProbability is float alreadyCachedValue) + { + return alreadyCachedValue; + } + else + { + _maxPatdownProbability = DefaultMaxPatdownProbability; + Identifier settingDefinitionIdentifier = nameof(PatdownProbabilityMax).ToIdentifier(); + Identifier attributeIdentifier = PatdownProbability.ToIdentifier(); + if (CampaignModePresets.TryGetAttribute(settingDefinitionIdentifier, attributeIdentifier, out XAttribute? attribute)) + { + _maxPatdownProbability = attribute.GetAttributeFloat(DefaultMaxPatdownProbability); + } + else + { + DebugConsole.ThrowError($"CampaignSettings: Can't find value for {attributeIdentifier} in {settingDefinitionIdentifier}"); + } + return _maxPatdownProbability ?? DefaultMaxPatdownProbability; + } + } + } public Dictionary SerializableProperties { get; private set; } @@ -119,5 +263,22 @@ namespace Barotrauma if (!characters.Any()) { return 0; } return characters.Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount)); } + + public struct MultiplierSettings + { + public float Min { get; set; } + public float Max { get; set; } + public float Step { get; set; } + } + + public static MultiplierSettings GetMultiplierSettings(string multiplierName) + { + if (_multiplierSettings.TryGetValue(multiplierName, out MultiplierSettings value)) + { + return value; + } + + return _multiplierSettings["default"]; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 7a5dd3d56..3aecb8b4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -390,11 +390,27 @@ namespace Barotrauma .Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)) .GetRandom(rand); dummyLocations = CreateDummyLocations(levelSeed, locationType); - if (!mission.Prefab.RequiredLocationFaction.IsEmpty && - FactionPrefab.Prefabs.TryGet(mission.Prefab.RequiredLocationFaction, out var factionPrefab)) + + if (!tryCreateFaction(mission.Prefab.RequiredLocationFaction, dummyLocations, static (loc, fac) => loc.Faction = fac)) { - dummyLocations[0].Faction = dummyLocations[1].Faction = new Faction(metadata: null, factionPrefab); + tryCreateFaction(locationType.Faction, dummyLocations, static (loc, fac) => loc.Faction = fac); + tryCreateFaction(locationType.SecondaryFaction, dummyLocations, static (loc, fac) => loc.SecondaryFaction = fac); } + static bool tryCreateFaction(Identifier factionIdentifier, Location[] locations, Action setter) + { + if (factionIdentifier.IsEmpty) { return false; } + if (!FactionPrefab.Prefabs.TryGet(factionIdentifier, out var prefab)) { return false; } + if (locations.Length == 0) { return false; } + + var newFaction = new Faction(metadata: null, prefab); + for (int i = 0; i < locations.Length; i++) + { + setter(locations[i], newFaction); + } + + return true; + } + randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; } @@ -473,6 +489,11 @@ namespace Barotrauma } foreach (Item item in items) { + if (item.GetComponent() is { } cb) + { + cb.Locked = true; + } + Wire wire = item.GetComponent(); if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; } } @@ -498,7 +519,7 @@ namespace Barotrauma string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0)); foreach (Mission mission in missions) { GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier); @@ -523,16 +544,24 @@ namespace Barotrauma GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}"); #endif var campaignMode = GameMode as CampaignMode; - if (campaignMode != null) - { - if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled) - { - GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Enabled"); - } - else - { - GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled"); - } + if (campaignMode != null) + { + GameAnalyticsManager.AddDesignEvent("CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount); + //log the multipliers as integers to reduce the number of distinct values + GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:FuelMultiplier:" + (int)(campaignMode.Settings.FuelMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:MissionRewardMultiplier:" + (int)(campaignMode.Settings.MissionRewardMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:CrewVitalityMultiplier:" + (int)(campaignMode.Settings.CrewVitalityMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:NonCrewVitalityMultiplier:" + (int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:OxygenMultiplier:" + (int)(campaignMode.Settings.OxygenMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShipyardPriceMultiplier:" + (int)(campaignMode.Settings.ShipyardPriceMultiplier * 100)); + GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShopPriceMultiplier:" + (int)(campaignMode.Settings.ShopPriceMultiplier * 100)); + bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome); if (firstTimeInBiome) { @@ -595,6 +624,19 @@ namespace Barotrauma GameMain.LuaCs.Hook.Call("roundStart"); EnableEventLogNotificationIcon(enabled: false); #endif + if (campaignMode is { ItemsRelocatedToMainSub: true }) + { +#if SERVER + GameMain.Server.SendChatMessage(TextManager.Get("itemrelocated").Value, ChatMessageType.ServerMessageBoxInGame); +#else + if (campaignMode.IsSinglePlayer) + { + new GUIMessageBox(string.Empty, TextManager.Get("itemrelocated")); + } +#endif + campaignMode.ItemsRelocatedToMainSub = false; + } + EventManager?.EventLog?.Clear(); if (campaignMode is { DivingSuitWarningShown: false } && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000) @@ -1012,46 +1054,54 @@ namespace Barotrauma public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null) { - GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); + if (Submarine.MainSub?.Info?.IsVanillaSubmarine() ?? false) + { + //don't log modded subs, that's a ton of extra data to collect + GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration); + } GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count ?? 0), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0), RoundDuration); foreach (Mission mission in missions) { GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration); } - if (Level.Loaded != null) + if (!ContentPackageManager.ModsEnabled) { - Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? - Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : - Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration); - } - - if (Submarine.MainSub != null) - { - Dictionary submarineInventory = new Dictionary(); - foreach (Item item in Item.ItemList) + if (Level.Loaded != null) { - var rootContainer = item.RootContainer ?? item; - if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; } - if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; } + Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? + Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : + Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration); + } - var holdable = item.GetComponent(); - if (holdable == null || holdable.Attached) { continue; } - var wire = item.GetComponent(); - if (wire != null && wire.Connections.Any(c => c != null)) { continue; } - - if (!submarineInventory.ContainsKey(item.Prefab)) + //disabled for now, we're collecting too many events and this is information we don't need atm + /*if (Submarine.MainSub != null) + { + Dictionary submarineInventory = new Dictionary(); + foreach (Item item in Item.ItemList) { - submarineInventory.Add(item.Prefab, 0); + var rootContainer = item.RootContainer ?? item; + if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; } + + var holdable = item.GetComponent(); + if (holdable == null || holdable.Attached) { continue; } + var wire = item.GetComponent(); + if (wire != null && wire.Connections.Any(c => c != null)) { continue; } + + if (!submarineInventory.ContainsKey(item.Prefab)) + { + submarineInventory.Add(item.Prefab, 0); + } + submarineInventory[item.Prefab]++; } - submarineInventory[item.Prefab]++; - } - foreach (var subItem in submarineInventory) - { - GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value); - } + foreach (var subItem in submarineInventory) + { + GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value); + } + }*/ } if (traitorResults.HasValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index fd7a6fcc9..887028517 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -278,6 +278,20 @@ namespace Barotrauma } } + /// + /// Used for purchasing upgrades from outside the upgrade store. + /// Doesn't deduct the credit, adds the upgrade to the pending list and performs a level sanity check. + /// + public void AddUpgradeExternally(UpgradePrefab prefab, UpgradeCategory category, int level) + { + int maxLevel = prefab.GetMaxLevelForCurrentSub(); + int currentLevel = GetUpgradeLevel(prefab, category); + if (currentLevel + 1 > maxLevel) { return; } + + PendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level)); + OnUpgradesChanged?.Invoke(this); + } + /// /// Purchases an item swap and handles logic for deducting the credit. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index 911fc6a4e..6de40b6ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -10,12 +10,14 @@ namespace Barotrauma Run, Crouch, InfoTab, Chat, RadioChat, CrewOrders, Ragdoll, Health, Grab, + DropItem, SelectNextCharacter, SelectPreviousCharacter, Voice, RadioVoice, LocalVoice, Deselect, Shoot, Command, + ContextualCommand, ToggleInventory, TakeOneFromInventorySlot, TakeHalfFromInventorySlot, @@ -23,6 +25,7 @@ namespace Barotrauma PreviousFireMode, ActiveChat, ToggleChatMode, - ChatBox + ChatBox, + ShowInteractionLabels } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 81ecc5a01..bfef76f61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -326,6 +326,8 @@ namespace Barotrauma.Items.Components ConnectWireBetweenPorts(); CreateJoint(true); + item.SendSignal("1", "on_dock"); + DockingTarget.Item.SendSignal("1", "on_dock"); #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) @@ -655,7 +657,8 @@ namespace Barotrauma.Items.Components hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); hulls[i] = new Hull(hullRects[i], subs[i]) { - RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch" + RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch", + AvoidStaying = true }; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -974,6 +977,7 @@ namespace Barotrauma.Items.Components item.linkedTo.Clear(); docked = false; + item.SendSignal("1", "on_undock"); Item.Submarine.RefreshOutdoorNodes(); Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 041465193..08e2ba6b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -165,6 +165,8 @@ namespace Barotrauma.Items.Components public bool IsHorizontal { get; private set; } + public bool IsConvexHullHorizontal => autoOrientGap && linkedGap != null ? !linkedGap.IsHorizontal : IsHorizontal; + [Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")] public Rectangle Window { get; set; } @@ -321,7 +323,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { if (item.Condition < RepairThreshold && item.GetComponent().HasRequiredItems(picker, addMessage: false)) { return true; } - if (requiredItems.None()) { return false; } + if (RequiredItems.None()) { return false; } if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; } return base.Pick(picker); } @@ -559,6 +561,7 @@ namespace Barotrauma.Items.Components public void RefreshLinkedGap() { + LinkedGap.Layer = item.Layer; LinkedGap.ConnectedDoor = this; if (autoOrientGap) { @@ -572,10 +575,10 @@ namespace Barotrauma.Items.Components { RefreshLinkedGap(); #if CLIENT - convexHull = new ConvexHull(doorRect, IsHorizontal, item); + convexHull = new ConvexHull(doorRect, IsConvexHullHorizontal, item); if (Window != Rectangle.Empty) { - convexHull2 = new ConvexHull(doorRect, IsHorizontal, item); + convexHull2 = new ConvexHull(doorRect, IsConvexHullHorizontal, item); } UpdateConvexHulls(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 846e92df5..8395c7c58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -77,6 +77,9 @@ namespace Barotrauma.Items.Components { Stem = 0b0000, CrossJunction = 0b1111, + HorizontalLine = 0b1010, + VerticalLine = 0b0101, + /*backwards compatibility, the vertical and horizontal "lane" used to be backwards*/ VerticalLane = 0b1010, HorizontalLane = 0b0101, TurnTopRight = 0b1001, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index bc06c7c4b..a7671a642 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -32,11 +32,6 @@ namespace Barotrauma.Items.Components private LocalizedString prevMsg; private Dictionary> prevRequiredItems; - //the distance from the holding characters elbow to center of the physics body of the item - protected Vector2 holdPos; - - protected Vector2 aimPos; - private float swingState; private Character prevEquipper; @@ -131,6 +126,9 @@ namespace Barotrauma.Items.Components get { return ConvertUnits.ToDisplayUnits(holdPos); } set { holdPos = ConvertUnits.ToSimUnits(value); } } + //the distance from the holding characters elbow to center of the physics body of the item + protected Vector2 holdPos; + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+ " Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")] @@ -139,6 +137,7 @@ namespace Barotrauma.Items.Components get { return ConvertUnits.ToDisplayUnits(aimPos); } set { aimPos = ConvertUnits.ToSimUnits(value); } } + protected Vector2 aimPos; protected float holdAngle; #if DEBUG @@ -258,12 +257,12 @@ namespace Barotrauma.Items.Components } canBePicked = true; + prevRequiredItems = new Dictionary>(RequiredItems); if (attachable) { prevMsg = DisplayMsg; prevPickKey = PickKey; - prevRequiredItems = new Dictionary>(requiredItems); if (item.Submarine != null) { @@ -320,7 +319,7 @@ namespace Barotrauma.Items.Components if (attachable) { prevMsg = DisplayMsg; - prevRequiredItems = new Dictionary>(requiredItems); + prevRequiredItems = new Dictionary>(RequiredItems); } } @@ -649,7 +648,7 @@ namespace Barotrauma.Items.Components DisplayMsg = prevMsg; PickKey = prevPickKey; - requiredItems = new Dictionary>(prevRequiredItems); + RequiredItems = new Dictionary>(prevRequiredItems); Attached = true; #if CLIENT @@ -667,7 +666,7 @@ namespace Barotrauma.Items.Components item.DrawDepthOffset = 0.0f; #endif //make the item pickable with the default pick key and with no specific tools/items when it's deattached - requiredItems.Clear(); + RequiredItems.Clear(); DisplayMsg = ""; PickKey = InputType.Select; #if CLIENT @@ -916,15 +915,25 @@ namespace Barotrauma.Items.Components bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker); if (aim) { - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle); + if (picker.AnimController.IsHoldingToRope && GetRope() is { Snapped: false } rope) + { + Vector2 targetPos = Submarine.GetRelativeSimPosition(picker, rope.Item); + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos, aim: true, holdAngle, aimAngle, targetPos: targetPos); + } + else + { + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: aimPos + swingPos, aim: true, holdAngle, aimAngle); + } } else { - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); - var rope = GetRope(); - if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null) + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, itemPos: holdPos + swingPos, aim: false, holdAngle); + if (GetRope() is { SnapWhenNotAimed: true } rope) { - rope.Snap(); + if (rope.Item.ParentInventory == null) + { + rope.Snap(); + } } } } @@ -1054,15 +1063,15 @@ namespace Barotrauma.Items.Components } var tempMsg = DisplayMsg; - var tempRequiredItems = requiredItems; + var tempRequiredItems = RequiredItems; DisplayMsg = prevMsg; - requiredItems = prevRequiredItems; + RequiredItems = prevRequiredItems; XElement saveElement = base.Save(parentElement); DisplayMsg = tempMsg; - requiredItems = tempRequiredItems; + RequiredItems = tempRequiredItems; return saveElement; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 67c3ff90d..a6495844c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components return; } holdable.Reattachable = false; - if (requiredItems.Any()) + if (RequiredItems.Any()) { holdable.PickingTime = float.MaxValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index e09cc8104..a5d94d611 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -91,7 +91,9 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { base.Equip(character); - reloadTimer = Math.Min(reload, 1.0f); + //force a wait of at least 1 second when equipping the weapon, so you can't "rapid-fire" by swapping between weapons + const float forcedDelayOnEquip = 1.0f; + reloadTimer = Math.Max(Math.Min(reload, forcedDelayOnEquip), reloadTimer); IsActive = true; } @@ -218,13 +220,12 @@ namespace Barotrauma.Items.Components AnimController ac = picker.AnimController; if (!hitting) { - bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim && - !UsageDisabledByRangedWeapon(picker); + bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim && !UsageDisabledByRangedWeapon(picker); if (aim) { UpdateSwingPos(deltaTime, out Vector2 swingPos); hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos + swingPos, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true); if (ac.InWater) { ac.LockFlipping(); @@ -233,7 +234,7 @@ namespace Barotrauma.Items.Components else { hitPos = 0; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle); } } else @@ -242,11 +243,11 @@ namespace Barotrauma.Items.Components hitPos -= deltaTime * 15f; if (Swing) { - ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: SwingPos, aim: false, hitPos, holdAngle); } else { - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle); } if (hitPos < -MathHelper.Pi) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 4935c05f5..5db4a58ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -82,9 +82,9 @@ namespace Barotrauma.Items.Components var abilityPickingTime = new AbilityItemPickingTime(PickingTime, item.Prefab); picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime); - if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) + if (RequiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) { - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Equipped]) { foreach (var heldItem in picker.HeldItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 315f8cc71..52a49838e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -95,6 +95,20 @@ namespace Barotrauma.Items.Components get; private set; } + + [Serialize(defaultValue: 1f, IsPropertySaveable.Yes, description: "Penalty multiplier to reload time when dual-wielding.")] + public float DualWieldReloadTimePenaltyMultiplier + { + get; + private set; + } + + [Serialize(defaultValue: 0f, IsPropertySaveable.Yes, description: "Additive penalty to accuracy (spread angle) when dual-wielding.")] + public float DualWieldAccuracyPenalty + { + get; + private set; + } private readonly IReadOnlySet suitableProjectiles; @@ -128,8 +142,11 @@ namespace Barotrauma.Items.Components : base(item, element) { item.IsShootable = true; - // TODO: should define this in xml if we have ranged weapons that don't require aim to use - item.RequireAimToUse = true; + if (element.Parent is { } parent) + { + item.RequireAimToUse = parent.GetAttributeBool(nameof(item.RequireAimToUse), true); + } + characterUsable = true; suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty()).ToHashSet(); if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) @@ -140,7 +157,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(ContentXElement element); + partial void InitProjSpecific(ContentXElement rangedWeaponElement); public override void Equip(Character character) { @@ -194,9 +211,28 @@ namespace Barotrauma.Items.Components float degreeOfFailure = MathHelper.Clamp(1.0f - DegreeOfSuccess(user), 0.0f, 1.0f); degreeOfFailure *= degreeOfFailure; float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction)); + if (user.IsDualWieldingRangedWeapons()) + { + spread += Math.Max(0f, ApplyDualWieldPenaltyReduction(user, DualWieldAccuracyPenalty, neutralValue: 0f)); + } return MathHelper.ToRadians(spread); } + /// + /// Lerps between the original penalty and a neutral value, which should be 1 for multipliers and 0 for additive penalties. + /// + /// The character to get stat values from + /// The original penalty value + /// Neutral value to lerp towards. Should be 1 for multipliers and 0 for additives. + /// + private static float ApplyDualWieldPenaltyReduction(Character character, float originalPenalty, float neutralValue) + { + float statAdjustmentPrc = character.GetStatValue(StatTypes.DualWieldingPenaltyReduction); + statAdjustmentPrc = MathHelper.Clamp(statAdjustmentPrc, 0f, 1f); + float reducedPenaltyMultiplier = MathHelper.Lerp(originalPenalty, neutralValue, statAdjustmentPrc); + return reducedPenaltyMultiplier; + } + private readonly List ignoredBodies = new List(); public override bool Use(float deltaTime, Character character = null) { @@ -208,22 +244,27 @@ namespace Barotrauma.Items.Components IsActive = true; float baseReloadTime = reload; float weaponSkill = character.GetSkillLevel("weapons"); - if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement) + + bool applyReloadFailure = ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement; + if (applyReloadFailure) { //Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5 float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1); baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure); } + + if (character.IsDualWieldingRangedWeapons()) + { + baseReloadTime *= Math.Max(1f, ApplyDualWieldPenaltyReduction(character, DualWieldReloadTimePenaltyMultiplier, neutralValue: 1f)); + } + ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier); currentChargeTime = 0f; - if (character != null) - { - var abilityRangedWeapon = new AbilityRangedWeapon(item); - character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon); - } + var abilityRangedWeapon = new AbilityRangedWeapon(item); + character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon); if (item.AiTarget != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 4f0686cef..906682b3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -90,12 +90,11 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } - [Serialize(100.0f, IsPropertySaveable.No, description: "How far two walls need to not be considered overlapping and to stop the ray.")] - public float MaxOverlappingWallDist - { - get; set; - } + public float MaxOverlappingWallDist { get; set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the tool detaches level resources (e.g. minerals). Acts as a multiplier on the speed: with a value of 2, detaching an item whose DeattachDuration is set to 30 seconds would take 15 seconds.")] + public float DeattachSpeed { get; set; } [Serialize(true, IsPropertySaveable.No, description: "Can the item hit doors.")] public bool HitItems { get; set; } @@ -171,7 +170,7 @@ namespace Barotrauma.Items.Components } } item.IsShootable = true; - item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); + item.RequireAimToUse = element.Parent.GetAttributeBool(nameof(item.RequireAimToUse), true); InitProjSpecific(element); } @@ -321,7 +320,7 @@ namespace Barotrauma.Items.Components private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall; if (!IgnoreCharacters) { collisionCategories |= Physics.CollisionCharacter; @@ -414,7 +413,7 @@ namespace Barotrauma.Items.Components break; } pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction; - if (FixBody(user, deltaTime, degreeOfSuccess, body)) + if (FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, body)) { lastPickedFraction = thisBodyFraction; if (bodyType != null) { lastHitType = bodyType; } @@ -452,7 +451,7 @@ namespace Barotrauma.Items.Components }, allowInsideFixture: true); pickedPosition = Submarine.LastPickedPosition; - FixBody(user, deltaTime, degreeOfSuccess, pickedBody); + FixBody(user, pickedPosition, deltaTime, degreeOfSuccess, pickedBody); lastPickedFraction = Submarine.LastPickedFraction; } @@ -543,7 +542,7 @@ namespace Barotrauma.Items.Components } } - private bool FixBody(Character user, float deltaTime, float degreeOfSuccess, Body targetBody) + private bool FixBody(Character user, Vector2 hitPosition, float deltaTime, float degreeOfSuccess, Body targetBody) { if (targetBody?.UserData == null) { return false; } @@ -600,7 +599,7 @@ namespace Barotrauma.Items.Components { if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall) { - levelWall.AddDamage(-LevelWallFixAmount * deltaTime, item.WorldPosition); + levelWall.AddDamage(-LevelWallFixAmount * deltaTime, ConvertUnits.ToDisplayUnits(hitPosition)); } return true; } @@ -661,10 +660,13 @@ namespace Barotrauma.Items.Components var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && - levelResource.requiredItems.Any() && + levelResource.RequiredItems.Any() && levelResource.HasRequiredItems(user, addMessage: false)) { - float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); + float addedDetachTime = deltaTime * + DeattachSpeed * + (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * + (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index e972c855e..33c3f6f37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -149,7 +149,7 @@ namespace Barotrauma.Items.Components if (aim || throwState == ThrowState.Initiated) { throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle); if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated) { throwState = ThrowState.Throwing; @@ -158,13 +158,13 @@ namespace Barotrauma.Items.Components else { throwAngle = ThrowAngleStart; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, holdAngle); } } else { throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); + ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, throwAngle); if (throwAngle < 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index f6868a2b4..6f0b50080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -50,10 +50,10 @@ namespace Barotrauma.Items.Components public readonly Dictionary> statusEffectLists; - public Dictionary> requiredItems; + public Dictionary> RequiredItems; public readonly List DisabledRequiredItems = new List(); - public List requiredSkills; + public readonly List RequiredSkills = new List(); private ItemComponent parent; public ItemComponent Parent @@ -288,9 +288,7 @@ namespace Barotrauma.Items.Components originalElement = element; name = element.Name.ToString(); SerializableProperties = SerializableProperty.GetProperties(this); - requiredItems = new Dictionary>(); - requiredSkills = new List(); - + RequiredItems = new Dictionary>(); #if CLIENT hasSoundsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; sounds = new Dictionary>(); @@ -338,7 +336,7 @@ namespace Barotrauma.Items.Components } else { - requiredSkills = component.requiredSkills; + RequiredSkills = component.RequiredSkills; } } @@ -391,7 +389,7 @@ namespace Barotrauma.Items.Components } Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", ""); - requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); + RequiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); break; case "statuseffect": statusEffectLists ??= new Dictionary>(); @@ -416,7 +414,7 @@ namespace Barotrauma.Items.Components void LoadStatusEffect(ContentXElement subElement) { - var statusEffect = StatusEffect.Load(subElement, item.Name); + var statusEffect = StatusEffect.Load(subElement, item.Name + ", " + GetType().Name); if (!statusEffectLists.TryGetValue(statusEffect.type, out List effectList)) { effectList = new List(); @@ -446,11 +444,11 @@ namespace Barotrauma.Items.Components } else { - if (!requiredItems.ContainsKey(ri.Type)) + if (!RequiredItems.ContainsKey(ri.Type)) { - requiredItems.Add(ri.Type, new List()); + RequiredItems.Add(ri.Type, new List()); } - requiredItems[ri.Type].Add(ri); + RequiredItems[ri.Type].Add(ri); } } else if (!allowEmpty) @@ -677,7 +675,7 @@ namespace Barotrauma.Items.Components public bool HasRequiredSkills(Character character, out Skill insufficientSkill) { - foreach (Skill skill in requiredSkills) + foreach (Skill skill in RequiredSkills) { float characterLevel = character.GetSkillLevel(skill.Identifier); if (characterLevel < skill.Level * GetSkillMultiplier()) @@ -698,7 +696,7 @@ namespace Barotrauma.Items.Components /// 0.5f if all the skills meet the skill requirements exactly, 1.0f if they're way above and 0.0f if way less public float DegreeOfSuccess(Character character) { - return DegreeOfSuccess(character, requiredSkills); + return DegreeOfSuccess(character, RequiredSkills); } /// @@ -733,16 +731,18 @@ namespace Barotrauma.Items.Components public virtual void FlipY(bool relativeToSub) { } /// - /// Shorthand for !HasRequiredContainedItems() + /// Returns true if the item is lacking required contained items, or if there's nothing with a non-zero condition inside. /// - public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false); + public bool IsEmpty(Character user) => + !HasRequiredContainedItems(user, addMessage: false) || + (Item.OwnInventory != null && !Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null) { - if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; } + if (!RequiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; } if (item.OwnInventory == null) { return false; } - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Contained]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Contained]) { if (!ri.CheckRequirements(user, item)) { @@ -767,8 +767,8 @@ namespace Barotrauma.Items.Components { if (character.IsBot && item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } - if (requiredItems.Count == 0) { return true; } - if (character.Inventory != null && requiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List relatedItems)) + if (RequiredItems.Count == 0) { return true; } + if (character.Inventory != null && RequiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List relatedItems)) { foreach (RelatedItem relatedItem in relatedItems) { @@ -813,13 +813,13 @@ namespace Barotrauma.Items.Components public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { - if (requiredItems.None()) { return true; } + if (RequiredItems.None()) { return true; } if (character.Inventory == null) { return false; } bool hasRequiredItems = false; bool canContinue = true; - if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) + if (RequiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) { - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Equipped]) { canContinue = CheckItems(ri, character.HeldItems); if (!canContinue) { break; } @@ -827,9 +827,9 @@ namespace Barotrauma.Items.Components } if (canContinue) { - if (requiredItems.ContainsKey(RelatedItem.RelationType.Picked)) + if (RequiredItems.ContainsKey(RelatedItem.RelationType.Picked)) { - foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Picked]) + foreach (RelatedItem ri in RequiredItems[RelatedItem.RelationType.Picked]) { if (!CheckItems(ri, character.Inventory.AllItems)) { break; } } @@ -1058,7 +1058,7 @@ namespace Barotrauma.Items.Components { XElement componentElement = new XElement(name); - foreach (var kvp in requiredItems) + foreach (var kvp in RequiredItems) { foreach (RelatedItem ri in kvp.Value) { @@ -1091,8 +1091,8 @@ namespace Barotrauma.Items.Components private void OverrideRequiredItems(ContentXElement element) { - var prevRequiredItems = new Dictionary>(requiredItems); - requiredItems.Clear(); + var prevRequiredItems = new Dictionary>(RequiredItems); + RequiredItems.Clear(); bool returnEmptyRequirements = false; #if CLIENT @@ -1117,11 +1117,11 @@ namespace Barotrauma.Items.Components newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor; } - if (!requiredItems.ContainsKey(newRequiredItem.Type)) + if (!RequiredItems.ContainsKey(newRequiredItem.Type)) { - requiredItems[newRequiredItem.Type] = new List(); + RequiredItems[newRequiredItem.Type] = new List(); } - requiredItems[newRequiredItem.Type].Add(newRequiredItem); + RequiredItems[newRequiredItem.Type].Add(newRequiredItem); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 8a164684c..66fef2fbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -673,7 +673,7 @@ namespace Barotrauma.Items.Components return false; } } - if (AutoInteractWithContained && character.SelectedItem == null) + if (AutoInteractWithContained && character.SelectedItem == null && Screen.Selected is not { IsEditor: true }) { foreach (Item contained in Inventory.AllItems) { @@ -708,7 +708,7 @@ namespace Barotrauma.Items.Components return false; } } - if (AutoInteractWithContained) + if (AutoInteractWithContained && Screen.Selected is not { IsEditor: true }) { foreach (Item contained in Inventory.AllItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 0666a7d45..062444e2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -34,10 +34,16 @@ namespace Barotrauma.Items.Components get { return outputContainer; } } + /// + /// Should the output items left in the deconstructor be automatically moved to the main sub at the end of the round + /// if the deconstructor is not in the main sub? + /// + public bool RelocateOutputToMainSub; + [Serialize(false, IsPropertySaveable.Yes)] public bool DeconstructItemsSimultaneously { get; set; } - [Editable, Serialize(1.0f, IsPropertySaveable.Yes)] + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)] public float DeconstructionSpeed { get; set; } public Deconstructor(Item item, ContentXElement element) @@ -290,6 +296,10 @@ namespace Barotrauma.Items.Components spawnedItem.AllowStealing = targetItem.AllowStealing; spawnedItem.OriginalOutpost = targetItem.OriginalOutpost; spawnedItem.SpawnedInCurrentOutpost = targetItem.SpawnedInCurrentOutpost; + if (RelocateOutputToMainSub && user is { AIController: HumanAIController humanAi }) + { + humanAi.HandleRelocation(spawnedItem); + } for (int i = 0; i < outputContainer.Capacity; i++) { var containedItem = outputContainer.Inventory.GetItemAt(i); @@ -318,7 +328,12 @@ namespace Barotrauma.Items.Components } } - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier); + if (targetItem.Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier); + } bool? result = GameMain.LuaCs.Hook.Call("item.deconstructed", targetItem, this, user, allowRemove); if (result == true) { return; } @@ -332,6 +347,10 @@ namespace Barotrauma.Items.Components foreach (Item outputItem in ic.Inventory.AllItemsMod) { tryPutInOutputSlots(outputItem); + if (RelocateOutputToMainSub && user != null && user.AIController is HumanAIController humanAi) + { + humanAi.HandleRelocation(outputItem); + } } } inputContainer.Inventory.RemoveItem(targetItem); @@ -439,11 +458,12 @@ namespace Barotrauma.Items.Components } } - private void SetActive(bool active, Character user = null) + public void SetActive(bool active, Character user = null, bool createNetworkEvent = false) { PutItemsToLinkedContainer(); this.user = user; + RelocateOutputToMainSub = false; if (inputContainer.Inventory.IsEmpty()) { active = false; } @@ -456,6 +476,10 @@ namespace Barotrauma.Items.Components { GameServer.Log(GameServer.CharacterLogName(user) + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction); } + if (createNetworkEvent) + { + item.CreateServerEvent(this); + } #endif if (!IsActive) { @@ -465,7 +489,11 @@ namespace Barotrauma.Items.Components #if CLIENT else { - HintManager.OnStartDeconstructing(user, this); + HintManager.OnStartDeconstructing(user, this); + if (Item.Submarine is { Info.IsOutpost: true } && user is { IsBot: true }) + { + HintManager.OnItemMarkedForRelocation(); + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 4331f4199..790838fe0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components private ItemContainer inputContainer, outputContainer; - [Serialize(1.0f, IsPropertySaveable.Yes)] + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 1000), Serialize(1.0f, IsPropertySaveable.Yes)] public float FabricationSpeed { get; set; } [Serialize(1.0f, IsPropertySaveable.Yes)] @@ -469,7 +469,10 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); - quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality(); + quality = + fabricatedItem.TargetItem.MaxStackSize > 1 ? + GetFabricatedItemQuality(fabricatedItem, user).Quality : + GetFabricatedItemQuality(fabricatedItem, user).RollQuality(); } int amount = (int)fabricationitemAmount.Value; @@ -490,7 +493,12 @@ namespace Barotrauma.Items.Components for (int i = 0; i < amount; i++) { float outCondition = fabricatedItem.OutCondition; - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); + if (fabricatedItem.TargetItem.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every fabricated item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); + } if (i < amountFittingContainer) { Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, @@ -577,11 +585,11 @@ namespace Barotrauma.Items.Components public static float CalculateBonusRollPercentage(float skillLevel, float target) => Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100); - public readonly record struct QualityResult(int Quality, float PlusOnePercentage, float PlusTwoPercentage) + public readonly record struct QualityResult(int Quality, bool HasRandomQuality, float PlusOnePercentage, float PlusTwoPercentage) { - public static readonly QualityResult Empty = new QualityResult(0, 0, 0); + public static readonly QualityResult Empty = new QualityResult(0, true, 0, 0); - public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f; + public bool HasRandomQualityRollChance => HasRandomQuality && (PlusOnePercentage > 0f || PlusTwoPercentage > 0f); // The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and // that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s @@ -676,9 +684,23 @@ namespace Barotrauma.Items.Components } } + bool hasRandomQuality = !(fabricatedItem.TargetItem.MaxStackSize > 1); //don't randomise items with a stacksize > 1 + float PlusOnePercentage = plusOne.Match(some: static f => f, none: static () => 0f); + float PlusTwoPercentage = plusTwo.Match(some: static f => f, none: static () => 0f); + + if (!hasRandomQuality && PlusOnePercentage > 0) + { + quality++; + if (PlusTwoPercentage > 0) + { + quality++; + } + } + return new QualityResult(quality, - PlusOnePercentage: plusOne.Match(some: static f => f, none: static () => 0f), - PlusTwoPercentage: plusTwo.Match(some: static f => f, none: static () => 0f)); + hasRandomQuality, + PlusOnePercentage, + PlusTwoPercentage); } partial void UpdateRequiredTimeProjSpecific(); @@ -690,6 +712,8 @@ namespace Barotrauma.Items.Components GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier)); } + private readonly HashSet usedIngredients = new HashSet(); + private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } @@ -733,22 +757,26 @@ namespace Barotrauma.Items.Components return false; } + //maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients + usedIngredients.Clear(); + return fabricableItem.RequiredItems.All(requiredItem => { - int availablePrefabsAmount = 0; + int availableItemsAmount = 0; foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { - if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; } - var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; - foreach (Item availablePrefab in availablePrefabs) + foreach (Item availableItem in availableItems) { - if (requiredItem.IsConditionSuitable(availablePrefab.ConditionPercentage)) + if (usedIngredients.Contains(availableItem)) { continue; } + if (requiredItem.IsConditionSuitable(availableItem.ConditionPercentage)) { - availablePrefabsAmount++; + usedIngredients.Add(availableItem); + availableItemsAmount++; } - if (availablePrefabsAmount >= requiredItem.Amount) + if (availableItemsAmount >= requiredItem.Amount) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index 47609785a..bfa77903b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -91,7 +91,7 @@ namespace Barotrauma.Items.Components ventList.Clear(); foreach (MapEntity entity in item.linkedTo) { - if (!(entity is Item linkedItem)) { continue; } + if (entity is not Item linkedItem) { continue; } Vent vent = linkedItem.GetComponent(); if (vent?.Item.CurrentHull == null) { continue; } @@ -132,5 +132,19 @@ namespace Barotrauma.Items.Components vent.IsActive = true; } } + + public float GetVentOxygenFlow(Vent targetVent) + { + if (ventList == null) + { + GetVents(); + } + foreach ((Vent vent, float hullVolume) in ventList) + { + if (vent != targetVent) { continue; } + return generatedAmount * 100.0f * (hullVolume / totalHullVolume); + } + return 0.0f; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index f76dace91..fccc5a2c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -215,10 +214,10 @@ namespace Barotrauma.Items.Components activePings[currentPingIndex].Direction = pingDirection; activePings[currentPingIndex].State = 0.0f; activePings[currentPingIndex].PrevPingRadius = 0.0f; - if (item.AiTarget != null) + foreach (AITarget aiTarget in GetAITargets()) { - item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f; - item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); + aiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f; + aiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); } item.Use(deltaTime); } @@ -231,10 +230,10 @@ namespace Barotrauma.Items.Components for (var pingIndex = 0; pingIndex < activePingsCount;) { - if (item.AiTarget != null) + foreach (AITarget aiTarget in GetAITargets()) { - float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom); - item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range)); + float range = MathUtils.InverseLerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom); + aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range)); } if (activePings[pingIndex].State > 1.0f) { @@ -254,6 +253,24 @@ namespace Barotrauma.Items.Components } } + private IEnumerable GetAITargets() + { + if (!UseTransducers) + { + if (item.AiTarget != null) { yield return item.AiTarget; } + } + else + { + foreach (var transducer in connectedTransducers) + { + if (transducer.Transducer.Item.AiTarget != null) + { + yield return transducer.Transducer.Item.AiTarget; + } + } + } + } + /// /// Power consumption of the sonar. Only consume power when active and adjust the consumption based on the sonar mode. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 9784e92b7..3780f7e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -22,6 +22,11 @@ namespace Barotrauma.Items.Components private const float AutoPilotMaxSpeed = 0.5f; private const float AIPilotMaxSpeed = 1.0f; + /// + /// How many units before crush depth the pressure warning is shown + /// + public const float PressureWarningThreshold = 500.0f; + /// /// How fast the steering vector adjusts when the nav terminal is operated by something else than a character (= signals) /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 9b3444649..dde0538e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -636,6 +636,8 @@ namespace Barotrauma.Items.Components } if (fixture.Body.UserData is VineTile) { return true; } if (fixture.CollidesWith == Category.None) { return true; } + //only collides with characters = probably an "outsideCollisionBlocker" created by a gap + if (fixture.CollidesWith == Physics.CollisionCharacter) { return true; } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } @@ -689,6 +691,8 @@ namespace Barotrauma.Items.Components } if (fixture.Body.UserData is VineTile) { return -1; } if (fixture.CollidesWith == Category.None) { return -1; } + //only collides with characters = probably an "outsideCollisionBlocker" created by a gap + if (fixture.CollidesWith == Physics.CollisionCharacter) { return -1; } if (fixture.Body.UserData is Item item) { if (item.Condition <= 0) { return -1; } @@ -945,9 +949,15 @@ namespace Barotrauma.Items.Components item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, collisionCategory: Physics.CollisionWall); + + Vector2 launchPosInCurrentCoordinateSpace = launchPos; + if (item.body.Submarine == null && LaunchSub != null) + { + launchPosInCurrentCoordinateSpace += ConvertUnits.ToSimUnits(LaunchSub.Position); + } if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0) + Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPosInCurrentCoordinateSpace, dir) > 0) { target = wallBody.FixtureList.First(); if (hits.Contains(target.Body)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 48a127a8d..65bc2c00c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -144,8 +144,10 @@ namespace Barotrauma.Items.Components private bool tinkeringPowersDevices; public bool TinkeringPowersDevices => tinkeringPowersDevices; - public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold; - public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2; + public bool IsBelowRepairThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold; + + public bool IsBelowRepairIconThreshold => item.ConditionPercentageRelativeToDefaultMaxCondition < RepairThreshold / 2; + public enum FixActions : int { @@ -186,6 +188,21 @@ namespace Barotrauma.Items.Components } } + // Modify damage (not stun) caused by repair failure based on campaign settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign + && statusEffectLists != null + && statusEffectLists.TryGetValue(ActionType.OnFailure, out var onFailureEffects)) + { + foreach (var effect in onFailureEffects) + { + foreach (Affliction affliction in effect.Afflictions) + { + if (affliction.Prefab.AfflictionType == Tags.Stun) { continue; } + affliction.Strength *= campaign.Settings.RepairFailMultiplier; + } + } + } + InitProjSpecific(element); } @@ -203,12 +220,12 @@ namespace Barotrauma.Items.Components { if (character == null) { return false; } - if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; } + if (statusEffectLists == null) { return true; } if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; } // unpowered (electrical) items can be repaired without a risk of electrical shock - if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) + if (RequiredSkills.Any(s => s != null && s.Identifier == "electrical")) { if (item.GetComponent() is Reactor reactor) { @@ -216,18 +233,29 @@ namespace Barotrauma.Items.Components } else if (item.GetComponent() is Powered powered && powered.Voltage < 0.1f) { - return true; + return true; } } - if (Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, requiredSkills)) { return true; } + bool success = Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, RequiredSkills); + ActionType actionType = success ? ActionType.OnSuccess : ActionType.OnFailure; - ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); - if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) + ApplyStatusEffectsAndCreateEntityEvent(this, actionType, character); + ApplyStatusEffectsAndCreateEntityEvent(this, ActionType.OnUse, character); + if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable holdable) { - h.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + ApplyStatusEffectsAndCreateEntityEvent(holdable, actionType, character); + ApplyStatusEffectsAndCreateEntityEvent(holdable, ActionType.OnUse, character); } - return false; + static void ApplyStatusEffectsAndCreateEntityEvent(ItemComponent ic, ActionType actionType, Character character) + { + ic.ApplyStatusEffects(actionType, 1.0f, character); + if (GameMain.NetworkMember is { IsServer: true } && ic.statusEffectLists != null && ic.statusEffectLists.ContainsKey(actionType)) + { + GameMain.NetworkMember.CreateEntityEvent(ic.Item, new Item.ApplyStatusEffectEventData(actionType, ic, character)); + } + } + return success; } public override float GetSkillMultiplier() @@ -251,9 +279,9 @@ namespace Barotrauma.Items.Components if (CurrentFixer == null) { return; } if (qteSuccess) { - item.Condition += RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); + item.Condition += RepairDegreeOfSuccess(CurrentFixer, RequiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); } - else if (Rand.Range(0.0f, 2.0f) > RepairDegreeOfSuccess(CurrentFixer, requiredSkills)) + else if (Rand.Range(0.0f, 2.0f) > RepairDegreeOfSuccess(CurrentFixer, RequiredSkills)) { ApplyStatusEffects(ActionType.OnFailure, 1.0f, CurrentFixer); #if SERVER @@ -283,12 +311,6 @@ namespace Barotrauma.Items.Components if (!CheckCharacterSuccess(character, bestRepairItem)) { GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, character)); - if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) - { - GameMain.Server?.CreateEntityEvent(bestRepairItem, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, h, character)); - } - return false; } @@ -449,7 +471,7 @@ namespace Barotrauma.Items.Components return; } - float successFactor = requiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, requiredSkills); + float successFactor = RequiredSkills.Count == 0 ? 1.0f : RepairDegreeOfSuccess(CurrentFixer, RequiredSkills); //item must have been below the repair threshold for the player to get an achievement or XP for repairing it if (IsBelowRepairThreshold) @@ -462,9 +484,16 @@ namespace Barotrauma.Items.Components } float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed); - if (requiredSkills.Any(static skill => skill.Identifier == "mechanical")) + foreach (Skill skill in RequiredSkills) { - talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed); + if (skill.Identifier == "mechanical") + { + talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed); + } + else if (skill.Identifier == "electrical") + { + talentMultiplier += CurrentFixer.GetStatValue(StatTypes.ElectricalRepairSpeed); + } } float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); @@ -493,7 +522,7 @@ namespace Barotrauma.Items.Components { if (wasBroken) { - foreach (Skill skill in requiredSkills) + foreach (Skill skill in RequiredSkills) { CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair); } @@ -527,7 +556,7 @@ namespace Barotrauma.Items.Components { if (wasGoodCondition) { - foreach (Skill skill in requiredSkills) + foreach (Skill skill in RequiredSkills) { float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier, @@ -578,11 +607,11 @@ namespace Barotrauma.Items.Components { if (character == null) { return 1.0f; } // kind of rough to keep this in update, but seems most robust - if (requiredSkills.Any(s => s != null && s.Identifier == "mechanical")) + if (RequiredSkills.Any(s => s != null && s.Identifier == "mechanical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical); } - if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) + if (RequiredSkills.Any(s => s != null && s.Identifier == "electrical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index fd7fe8aca..96b34c9f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -11,8 +11,8 @@ namespace Barotrauma.Items.Components { private ISpatialEntity source; private Item target; - private Vector2? launchDir; + private float currentRopeLength; private void SetSource(ISpatialEntity source) { @@ -73,6 +73,13 @@ namespace Barotrauma.Items.Components get; set; } + + [Serialize(200.0f, IsPropertySaveable.No, description: "At which distance the user stops pulling the target?")] + public float MinPullDistance + { + get; + set; + } [Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")] public float MaxAngle @@ -108,7 +115,8 @@ namespace Barotrauma.Items.Components get; set; } - + + private bool isReelingIn; private bool snapped; public bool Snapped { @@ -134,6 +142,15 @@ namespace Barotrauma.Items.Components { snapTimer = 0; } + else if (target != null && source != null && target != source) + { +#if CLIENT + // Play a sound at both ends. Initially tested playing the sound in the middle when the rope snaps in the middle, + // but I think it's more important to ensure that the players hear the sound. + PlaySound(snapSound, source.WorldPosition); + PlaySound(snapSound, target.WorldPosition); +#endif + } } } @@ -156,14 +173,17 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); IsActive = true; } - + public override void Update(float deltaTime, Camera cam) { - var user = item.GetComponent()?.User; + UpdateProjSpecific(); + isReelingIn = false; + Character user = item.GetComponent()?.User; if (source == null || target == null || target.Removed || - (source is Entity sourceEntity && sourceEntity.Removed) || - (source is Limb limb && limb.Removed) || - (user != null && user.Removed)) + source is Entity { Removed: true } || + source is Limb { Removed: true } || + user is null || + user is { Removed: true }) { ResetSource(); target = null; @@ -191,11 +211,8 @@ namespace Barotrauma.Items.Components if (MaxAngle < 180 && lengthSqr > 2500) { - if (launchDir == null) - { - launchDir = diff; - } - float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff)); + launchDir ??= diff; + float angle = MathHelper.ToDegrees(launchDir.Value.Angle(diff)); if (angle > MaxAngle) { Snap(); @@ -262,8 +279,8 @@ namespace Barotrauma.Items.Components } Vector2 forceDir = diff; - float distance = diff.Length(); - if (distance > 0.001f) + currentRopeLength = diff.Length(); + if (currentRopeLength > 0.001f) { forceDir = Vector2.Normalize(forceDir); } @@ -277,87 +294,162 @@ namespace Barotrauma.Items.Components { float targetMass = float.MaxValue; Character targetCharacter = null; - if (projectile.StickTarget.UserData is Limb targetLimb) + switch (projectile.StickTarget.UserData) { - targetCharacter = targetLimb.character; - targetMass = targetLimb.ragdoll.Mass; - } - else if (projectile.StickTarget.UserData is Character character) - { - targetCharacter = character; - targetMass = character.Mass; - } - else if (projectile.StickTarget.UserData is Item item) - { - targetMass = projectile.StickTarget.Mass; + case Limb targetLimb: + targetCharacter = targetLimb.character; + targetMass = targetLimb.ragdoll.Mass; + break; + case Character character: + targetCharacter = character; + targetMass = character.Mass; + break; + case Item _: + targetMass = projectile.StickTarget.Mass; + break; } if (projectile.StickTarget.BodyType != BodyType.Dynamic) { targetMass = float.MaxValue; } - if (targetMass > TargetMinMass) + if (!snapped) { - if (Math.Abs(SourcePullForce) > 0.001f) + user.AnimController.HoldToRope(); + if (targetCharacter != null) { - var sourceBody = GetBodyToPull(source); - if (sourceBody != null) + targetCharacter.AnimController.DragWithRope(); + } + if (user.InWater) + { + user.AnimController.HangWithRope(); + } + } + if (Math.Abs(SourcePullForce) > 0.001f && targetMass > TargetMinMass) + { + // This should be the main collider. + var sourceBody = GetBodyToPull(source); + if (sourceBody != null) + { + isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false }; + if (isReelingIn) { - if (user != null && user.InWater) + float pullForce = SourcePullForce; + if (!user.InWater) + { + // Apply a tiny amount to the character holding the rope, so that the connection "feels" more real. + pullForce *= 0.1f; + } + float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength); + float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce; + sourceBody.ApplyForce(forceDir * force); + // Take the target velocity into account. + PhysicsBody targetBody = GetBodyToPull(target); + if (targetBody != null) { - if (user.IsRagdolled) - { - // Reel in towards the target. - user.AnimController.Hang(); - float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce; - sourceBody.ApplyForce(forceDir * force); - } - // Take the target velocity into account. if (targetCharacter != null) { - var myCollider = user.AnimController.Collider; - var targetCollider = targetCharacter.AnimController.Collider; - if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero) + if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero) { - if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0) + Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity); + float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir); + if (movementDot < 0) { - myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass); + // Pushing to a different dir -> add some counter force + const float multiplier = 5; + float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor); + sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor); + } + float forceDot = Vector2.Dot(forceDir, targetDir); + if (forceDot > 0) + { + // Pulling to the same dir -> add extra force + float targetSpeed = targetBody.LinearVelocity.Length(); + const float multiplier = 25; + sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor); + } + float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition); + const float minDist = 1; + const float maxDist = 10; + if (colliderMainLimbDistance > minDist) + { + // Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported). + float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance)); + Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f; + user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce); } } } else { - var targetBody = GetBodyToPull(target); - if (targetBody != null) - { - sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); - } + sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); } } } } } - if (Math.Abs(TargetPullForce) > 0.001f) + if (Math.Abs(TargetPullForce) > 0.001f && !user.IsRagdolled) { - var targetBody = GetBodyToPull(target); + PhysicsBody targetBody = GetBodyToPull(target); + if (targetBody == null) { return; } bool lerpForces = LerpForces; - if (!lerpForces && user != null && targetCharacter != null && !user.AnimController.InWater) + float maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.25f; + // The distance where we start pulling with max force. + float maxPullDistance = MaxLength / 3; + float minPullDistance = MinPullDistance; + const float absoluteMinPullDistance = 50; + if (targetCharacter != null) { - if ((forceDir.X < 0) != (user.AnimController.Dir < 0)) + if (targetCharacter.IsRagdolled || targetCharacter.IsUnconscious) { - // Prevents rubberbanding horizontally when dragging a corpse. - lerpForces = true; + if (!targetCharacter.InWater) + { + // Limits the velocity of ragdolled characters on ground/air, because otherwise they tend to move with too high forces. + maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.075f; + } + } + else + { + // Target alive and kicking -> Use the absolute min pull distance and full forces to pull. + // Keep some lerping, because it results into smoothing when the target is close by. + minPullDistance = absoluteMinPullDistance; + maxPullDistance = 200; } } - float force = lerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce; - targetBody?.ApplyForce(-forceDir * force); - var targetRagdoll = targetCharacter?.AnimController; - if (targetRagdoll?.Collider != null && (targetRagdoll.InWater || targetRagdoll.OnGround)) + minPullDistance = MathHelper.Max(minPullDistance, absoluteMinPullDistance); + if (currentRopeLength < minPullDistance) { return; } + maxPullDistance = MathHelper.Max(minPullDistance * 2, maxPullDistance); + float force = lerpForces + ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(minPullDistance, maxPullDistance, currentRopeLength)) + : TargetPullForce; + targetBody.ApplyForce(-forceDir * force, maxVelocity); + AnimController targetRagdoll = targetCharacter?.AnimController; + if (targetRagdoll?.Collider != null) { - targetRagdoll.Collider.ApplyForce(-forceDir * force * 3); + isReelingIn = true; + if (targetRagdoll.InWater || targetRagdoll.OnGround) + { + float forceMultiplier = 1; + if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated) + { + // Pulling the main collider requires higher forces when the target is trying to move away. + Vector2 targetMovement = targetCharacter.AnimController.TargetMovement; + float dot = Vector2.Dot(Vector2.Normalize(targetMovement), forceDir); + if (dot > 0) + { + const float constMultiplier = 2.5f; + float targetVelocity = targetMovement.Length(); + float massFactor = Math.Max((float)Math.Log(targetCharacter.Mass / 10), 1); + forceMultiplier = Math.Max(targetVelocity * massFactor * constMultiplier * dot, 1); + } + } + targetRagdoll.Collider.ApplyForce(-forceDir * force * forceMultiplier, maxVelocity); + } } } } } + + partial void UpdateProjSpecific(); public override void UpdateBroken(float deltaTime, Camera cam) { @@ -409,32 +501,22 @@ namespace Barotrauma.Items.Components { if (target is Item targetItem) { - if (targetItem.ParentInventory is CharacterInventory characterInventory && - characterInventory.Owner is Character ownerCharacter) + if (targetItem.ParentInventory is CharacterInventory { Owner: Character ownerCharacter }) { if (ownerCharacter.Removed) { return null; } return ownerCharacter.AnimController.Collider; } var projectile = targetItem.GetComponent(); - if (projectile != null && projectile.StickTarget != null) + if (projectile is { StickTarget: not null }) { - if (projectile.StickTarget.UserData is Structure structure) + return projectile.StickTarget.UserData switch { - return structure.Submarine?.PhysicsBody; - } - else if (projectile.StickTarget.UserData is Submarine sub) - { - return sub.PhysicsBody; - } - else if (projectile.StickTarget.UserData is Item item) - { - return item.body; - } - else if (projectile.StickTarget.UserData is Limb limb) - { - return limb.body; - } - return null; + Structure structure => structure.Submarine?.PhysicsBody, + Submarine sub => sub.PhysicsBody, + Item item => item.body, + Limb limb => limb.body, + _ => null + }; } if (targetItem.body != null) { return targetItem.body; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs index 69c395e3c..64fa0d3bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CircuitBox.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components public readonly List InputOutputNodes = new(); + public readonly List Labels = new(); + public readonly List Wires = new List(); public override bool IsActive => true; @@ -80,6 +82,9 @@ namespace Barotrauma.Items.Components public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true); + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked circuit boxes can only be viewed and not interacted with.")] + public bool Locked { get; set; } + public CircuitBox(Item item, ContentXElement element) : base(item, element) { containers = item.GetComponents().ToArray(); @@ -176,6 +181,9 @@ namespace Barotrauma.Items.Components case "outputnode": LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement); break; + case "label": + Labels.Add(CircuitBoxLabelNode.LoadFromXML(subElement, this)); + break; } } @@ -204,11 +212,14 @@ namespace Barotrauma.Items.Components { Components.Clear(); Wires.Clear(); + Labels.Clear(); - foreach (var origComp in original.Components) + foreach (var label in original.Labels) { - var newComponent = new CircuitBoxComponent(origComp.ID, clonedContainedItems[origComp.Item.ID], origComp.Position, this, origComp.UsedResource); - Components.Add(newComponent); + var newLabel = new CircuitBoxLabelNode(label.ID, label.Color, label.Position, this); + newLabel.EditText(label.HeaderText, label.BodyText); + newLabel.ApplyResize(label.Size, label.Position); + Labels.Add(newLabel); } for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++) @@ -219,10 +230,19 @@ namespace Barotrauma.Items.Components cloneNode.Position = origNode.Position; } + if (!clonedContainedItems.Any()) { return; } + + foreach (var origComp in original.Components) + { + if (!clonedContainedItems.TryGetValue(origComp.Item.ID, out var clonedItem)) { continue; } + var newComponent = new CircuitBoxComponent(origComp.ID, clonedItem, origComp.Position, this, origComp.UsedResource); + Components.Add(newComponent); + } + foreach (var origWire in original.Wires) { Option to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this), - from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this); + from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this); if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn)) { @@ -230,7 +250,8 @@ namespace Barotrauma.Items.Components continue; } - var newWire = new CircuitBoxWire(this, origWire.ID, origWire.BackingWire.Select(w => clonedContainedItems[w.ID]), fromConn, toConn, origWire.UsedItemPrefab); + var wireItem = origWire.BackingWire.Select(w => clonedContainedItems[w.ID]); + var newWire = new CircuitBoxWire(this, origWire.ID, wireItem, fromConn, toConn, origWire.UsedItemPrefab); Wires.Add(newWire); } } @@ -254,6 +275,11 @@ namespace Barotrauma.Items.Components componentElement.Add(wire.Save()); } + foreach (var label in Labels) + { + componentElement.Add(label.Save()); + } + return componentElement; } @@ -324,6 +350,36 @@ namespace Barotrauma.Items.Components return true; } + private void AddLabelInternal(ushort id, Color color, Vector2 pos, NetLimitedString header, NetLimitedString body) + { + var newLabel = new CircuitBoxLabelNode(id, color, pos, this); + newLabel.EditText(header, body); + Labels.Add(newLabel); + OnViewUpdateProjSpecific(); + } + + private void RemoveLabelInternal(IReadOnlyCollection ids) + { + foreach (CircuitBoxLabelNode node in Labels.ToImmutableArray()) + { + if (!ids.Contains(node.ID)) { continue; } + Labels.Remove(node); + } + OnViewUpdateProjSpecific(); + } + + private void ResizeLabelInternal(ushort id, Vector2 pos, Vector2 size) + { + size = Vector2.Max(size, CircuitBoxLabelNode.MinSize); + foreach (CircuitBoxLabelNode node in Labels) + { + if (node.ID != id) { continue; } + node.ApplyResize(size, pos); + break; + } + OnViewUpdateProjSpecific(); + } + private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection); private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab) @@ -380,6 +436,18 @@ namespace Barotrauma.Items.Components private void AddWireDirect(ushort id, ItemPrefab prefab, Option backingItem, CircuitBoxConnection one, CircuitBoxConnection two) => Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab)); + private void RenameLabelInternal(ushort id, Color color, NetLimitedString header, NetLimitedString body) + { + foreach (CircuitBoxLabelNode node in Labels) + { + if (node.ID != id) { continue; } + + node.EditText(header, body); + node.Color = color; + break; + } + } + private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Character? user, Action? onItemSpawned) { if (id is ICircuitBoxIdentifiable.NullComponentID) @@ -426,6 +494,21 @@ namespace Barotrauma.Items.Components ClearSelectionFor(characterId, Components); ClearSelectionFor(characterId, InputOutputNodes); ClearSelectionFor(characterId, Wires); + ClearSelectionFor(characterId, Labels); + } + + private void SelectLabelsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) + { + if (overwrite) { ClearSelectionFor(characterId, Labels); } + + if (!ids.Any()) { return; } + + foreach (CircuitBoxLabelNode node in Labels) + { + if (!ids.Contains(node.ID)) { continue; } + + node.SetSelected(Option.Some(characterId)); + } } private void SelectComponentsInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) @@ -444,7 +527,8 @@ namespace Barotrauma.Items.Components private void UpdateSelections(ImmutableDictionary> nodeIds, ImmutableDictionary> wireIds, - ImmutableDictionary> inputOutputs) + ImmutableDictionary> inputOutputs, + ImmutableDictionary> labels) { foreach (var wire in Wires) { @@ -474,6 +558,13 @@ namespace Barotrauma.Items.Components node.SetSelected(selectedBy); } + + foreach (var node in Labels) + { + if (!labels.TryGetValue(node.ID, out var selectedBy)) { continue; } + + node.SetSelected(selectedBy); + } } private void SelectWiresInternal(IReadOnlyCollection ids, ushort characterId, bool overwrite) @@ -555,6 +646,7 @@ namespace Barotrauma.Items.Components private void MoveNodesInternal(IReadOnlyCollection ids, IReadOnlyCollection ios, + IReadOnlyCollection labels, Vector2 moveAmount) { IEnumerable nodes = Components.Where(node => ids.Contains(node.ID)); @@ -563,6 +655,11 @@ namespace Barotrauma.Items.Components node.Position += moveAmount; } + foreach (var label in Labels.Where(n => labels.Contains(n.ID))) + { + label.Position += moveAmount; + } + foreach (var io in InputOutputNodes) { @@ -635,7 +732,7 @@ namespace Barotrauma.Items.Components } } - public static ImmutableArray GetSortedCircuitBoxSortedItemsFromPlayer(Character? character) + public static ImmutableArray GetSortedCircuitBoxItemsFromPlayer(Character? character) => character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true) .OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit) .ToImmutableArray() ?? ImmutableArray.Empty; @@ -651,7 +748,7 @@ namespace Barotrauma.Items.Components { if (character is null) { return Option.None; } - return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxSortedItemsFromPlayer(character)); + return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxItemsFromPlayer(character)); } public static Option GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray playerItems) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index f96dc53e4..3923350ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -292,9 +292,8 @@ namespace Barotrauma.Items.Components refSub = attachTarget?.Submarine; } - Vector2 nodePos = refSub == null ? - newConnection.Item.Position : - newConnection.Item.Position - refSub.HiddenSubPosition; + Vector2 nodePos = RoundNode(newConnection.Item.Position); + if (refSub != null) { nodePos -= refSub.HiddenSubPosition; } if (nodes.Count > 0 && nodes[0] == nodePos) { return; } if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { return; } @@ -469,9 +468,7 @@ namespace Barotrauma.Items.Components Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition; mouseDiff = mouseDiff.ClampLength(MaxAttachDistance); - return new Vector2( - MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X), - MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y)); + return RoundNode(user.Position + mouseDiff); } public override bool Use(float deltaTime, Character character = null) @@ -662,11 +659,14 @@ namespace Barotrauma.Items.Components Drawable = sections.Count > 0; } - private Vector2 RoundNode(Vector2 position) + private static Vector2 RoundNode(Vector2 position) { - position.X = MathUtils.Round(position.X, Submarine.GridSize.X / 2.0f); - position.Y = MathUtils.Round(position.Y, Submarine.GridSize.Y / 2.0f); - return position; + Vector2 halfGrid = Submarine.GridSize / 2; + + position += halfGrid; + position.X = MathUtils.RoundTowardsClosest(position.X, Submarine.GridSize.X / 2.0f); + position.Y = MathUtils.RoundTowardsClosest(position.Y, Submarine.GridSize.Y / 2.0f); + return position - halfGrid; } public void SetConnectedDirty() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 8f1228208..e6a875f6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -143,7 +143,7 @@ namespace Barotrauma.Items.Components private bool OnCollision(Fixture sender, Fixture other, Contact contact) { - if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; } + if (LevelTrigger.GetEntity(other) is not Entity entity) { return false; } if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; } triggerers.Add(entity); return true; @@ -151,7 +151,7 @@ namespace Barotrauma.Items.Components private void OnSeparation(Fixture sender, Fixture other, Contact contact) { - if (!(LevelTrigger.GetEntity(other) is Entity entity)) + if (LevelTrigger.GetEntity(other) is not Entity entity) { return; } @@ -233,7 +233,17 @@ namespace Barotrauma.Items.Components } else if (triggerer is Character c) { - ApplyForce(c.AnimController.Collider); + if (c.AnimController.Collider.BodyType == BodyType.Dynamic) + { + if (c.AnimController.Collider.Enabled) + { + ApplyForce(c.AnimController.Collider); + } + foreach (var limb in c.AnimController.Limbs) + { + ApplyForce(limb.body, multiplier: limb.Mass * c.AnimController.Collider.Mass / c.AnimController.Mass); + } + } } else if (triggerer is Submarine s) { @@ -248,13 +258,13 @@ namespace Barotrauma.Items.Components item.SendSignal(IsActive ? "1" : "0", "state_out"); } - private void ApplyForce(PhysicsBody body) + private void ApplyForce(PhysicsBody body, float multiplier = 1.0f) { Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); if (diff.LengthSquared() < 0.0001f) { return; } float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f; if (distanceFactor <= 0.0f) { return; } - Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff); + Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier; if (force.LengthSquared() < 0.01f) { return; } body.ApplyForce(force); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 180487e83..001fd3eef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -338,6 +338,11 @@ namespace Barotrauma.Items.Components } } + /// + /// How deep down does the item protect from pressure? Determined by status effects. + /// + public readonly float PressureProtection; + public Wearable(Item item, ContentXElement element) : base(item, element) { this.item = item; @@ -415,6 +420,12 @@ namespace Barotrauma.Items.Components WearableStatValues.TryAdd(statType, statValue); } break; + case "statuseffect": + if (subElement.GetAttributeString("Target", string.Empty).ToLowerInvariant().Contains("character")) + { + PressureProtection = Math.Max(subElement.GetAttributeFloat(nameof(PressureProtection), 0), PressureProtection); + } + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs new file mode 100644 index 000000000..ad61ca8e2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ContainerTagPrefab.cs @@ -0,0 +1,149 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + internal class ContainerTagPrefab : Prefab + { + public static readonly PrefabCollection Prefabs = new(); + + public readonly LocalizedString Name; + public readonly LocalizedString Description; + public readonly Identifier Category; + public readonly int RecommendedAmount; + public readonly bool WarnIfLess; + + private static readonly Dictionary categoryToSubmarineType = new() + { + { new Identifier("Submarine"), SubmarineType.Player.ToIdentifier() }, + { new Identifier("AbandonedOutpost"), SubmarineType.OutpostModule.ToIdentifier() }, + { new Identifier("Ruin"), SubmarineType.OutpostModule.ToIdentifier() }, + { new Identifier("Enemy"), SubmarineType.EnemySubmarine.ToIdentifier() } + }; + + public bool IsRecommendedForSub(Submarine sub) + { + var type = sub.Info?.Type ?? SubmarineType.Player; + Identifier category = categoryToSubmarineType.GetValueOrDefault(Category, Category); + return type.ToIdentifier() == category; + } + + public ContainerTagPrefab(ContentXElement element, ContainerTagFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + Category = element.GetAttributeIdentifier("category", ""); + + var nameOverride = element.GetAttributeString("nameidentifier", string.Empty); + + Name = string.IsNullOrEmpty(nameOverride) + ? TextManager.Get($"tagname.{Identifier}").Fallback(Identifier.Value) + : TextManager.Get($"tagname.{nameOverride}").Fallback(Identifier.Value); + + Description = string.IsNullOrEmpty(nameOverride) + ? TextManager.Get($"tagdescription.{Identifier}") + : TextManager.Get($"tagdescription.{nameOverride}"); + + var suffix = element.GetAttributeString("suffix", string.Empty); + if (!string.IsNullOrEmpty(suffix)) + { + Name = TextManager.GetWithVariable($"{suffix}.tagnamesuffix", "[tagname]", Name); + } + + RecommendedAmount = element.GetAttributeInt("recommendedamount", 0); + WarnIfLess = element.GetAttributeBool("warnifless", true); + } + + public readonly record struct ItemAndProbability(ItemPrefab Prefab, float Probability, float CampaignProbability); + + public ImmutableArray GetItemsAndSpawnProbabilities() + { + var items = ImmutableArray.CreateBuilder(); + foreach (ItemPrefab ip in ItemPrefab.Prefabs) + { + bool found = false; + float spawnProbability = 0f; + float campaignSpawnProbability = 0f; + + foreach (PreferredContainer pc in ip.PreferredContainers) + { + if (!pc.Primary.Contains(Identifier) && !pc.Secondary.Contains(Identifier)) { continue; } + + found = true; + spawnProbability = Math.Max(pc.SpawnProbability, spawnProbability); + if (!pc.NotCampaign) + { + campaignSpawnProbability = Math.Max(spawnProbability, campaignSpawnProbability); + } + + if (!pc.NotCampaign || pc.CampaignOnly) + { + campaignSpawnProbability = Math.Max(pc.SpawnProbability, campaignSpawnProbability); + } + } + + if (found) + { + items.Add(new ItemAndProbability(ip, spawnProbability, campaignSpawnProbability)); + } + } + return items.ToImmutable(); + } + + public static void CheckForContainerTagErrors() + { + var allContainerTagsInTheGame = new HashSet(); + var vanillaContainerTags = new HashSet(); + + foreach (var prefab in ItemPrefab.Prefabs) + { + foreach (Identifier tag in prefab.PreferredContainers.SelectMany(pc => Enumerable.Union(pc.Primary, pc.Secondary))) + { + allContainerTagsInTheGame.Add(tag); + if (prefab.ContentPackage == GameMain.VanillaContent && !TagExistsInItemOrCharacterPrefab(tag)) + { + vanillaContainerTags.Add(tag); + } + } + } + + static bool TagExistsInItemOrCharacterPrefab(Identifier tag) + { + if (CharacterPrefab.Prefabs.TryGet(tag, out _)) + { + return true; + } + + foreach (var prefab in ItemPrefab.Prefabs) + { + if (prefab.Tags.Contains(tag) || prefab.Identifier == tag) { return true; } + } + + return false; + } + + // Find container tags that are defined in a ContainerTagPrefab but not used in any item prefabs. + foreach (var prefab in Prefabs) + { + if (!allContainerTagsInTheGame.Contains(prefab.Identifier)) + { + DebugConsole.ThrowError($"Container tag \"{prefab.Identifier}\" defined in ContainerTagPrefab is not used in any item prefabs, did you misspell it?", contentPackage: prefab.ContentPackage); + } + } + + // Find container tags that are used in vanilla item prefabs but not defined in a ContainerTagPrefab. + // We only check vanilla item prefabs because we don't want to force modders to define all vanilla container tags. + foreach (var vanillaTag in vanillaContainerTags) + { + if (Prefabs.All(p => p.Identifier != vanillaTag)) + { + DebugConsole.ThrowError($"Container tag \"{vanillaTag}\" is used in vanilla item prefabs but not defined in a ContainerTagPrefab."); + } + } + } + + public override void Dispose() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 6ed855f34..a9f1cf8a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -44,6 +44,13 @@ namespace Barotrauma /// public static IReadOnlyCollection CleanableItems => cleanableItems; + private static readonly HashSet deconstructItems = new HashSet(); + + /// + /// Items that have been marked for deconstruction + /// + public static HashSet DeconstructItems => deconstructItems; + private static readonly List sonarVisibleItems = new List(); /// @@ -184,6 +191,11 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; + /// + /// Helper variable for handling max condition multipliers from campaign settings + /// + private readonly float conditionMultiplierCampaign = 1.0f; + public Action OnInteract; public Dictionary SerializableProperties { get; protected set; } @@ -275,6 +287,11 @@ namespace Barotrauma } } + /// + /// Note that this is not a instance, just the current name of the item as a string. + /// If you e.g. set this as the text in a textbox, it will not update automatically when the language is changed. + /// If you want that to happen, use instead. + /// public override string Name { get { return base.Prefab.Name.Value; } @@ -558,11 +575,10 @@ namespace Barotrauma public Color? HighlightColor; - [Serialize("", IsPropertySaveable.Yes)] - /// /// Can be used to modify the AITarget's label using status effects /// + [Serialize("", IsPropertySaveable.Yes)] public string SonarLabel { get { return AiTarget?.SonarLabel?.Value ?? ""; } @@ -587,20 +603,20 @@ namespace Barotrauma } } - [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sound range /// + [Serialize(0.0f, IsPropertySaveable.No)] public new float SoundRange { get { return aiTarget == null ? 0.0f : aiTarget.SoundRange; } set { if (aiTarget != null) { aiTarget.SoundRange = Math.Max(0.0f, value); } } } - [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sight range /// + [Serialize(0.0f, IsPropertySaveable.No)] public new float SightRange { get { return aiTarget == null ? 0.0f : aiTarget.SightRange; } @@ -631,6 +647,14 @@ namespace Barotrauma get; set; } + /// + /// Can be set by status effects to prevent bots from cleaning up the item + /// + public bool DontCleanUp + { + get; set; + } + public Color Color { get { return spriteColor; } @@ -640,6 +664,18 @@ namespace Barotrauma public float MaxCondition { get; private set; } public float ConditionPercentage { get; private set; } + /// + /// Condition percentage disregarding MaxRepairConditionMultiplier (i.e. this can go above 100% if the item is repaired beyond the normal maximum) + /// + public float ConditionPercentageRelativeToDefaultMaxCondition + { + get + { + float defaultMaxCondition = MaxCondition / MaxRepairConditionMultiplier; + return MathUtils.Percentage(Condition, defaultMaxCondition); + } + } + private float offsetOnSelectedMultiplier = 1.0f; [Serialize(1.0f, IsPropertySaveable.No)] @@ -677,6 +713,9 @@ namespace Barotrauma RecalculateConditionValues(); } } + + [Serialize(false, IsPropertySaveable.Yes)] + private bool HasBeenInstantiatedOnce { get; set; } //the default value should be Prefab.Health, but because we can't use it in the attribute, //we'll just use NaN (which does nothing) and set the default value in the constructor/load @@ -1078,7 +1117,7 @@ namespace Barotrauma string collisionCategoryStr = subElement.GetAttributeString("collisioncategory", null); Category collisionCategory = Physics.CollisionItem; - Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; + Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionRepairableWall; if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) { //force collision category to Character to allow projectiles and weapons to hit @@ -1267,6 +1306,27 @@ namespace Barotrauma GameMain.LuaCs.Hook.Call("item.created", this); ApplyStatusEffects(ActionType.OnSpawn, 1.0f); + + // Set max condition multipliers from campaign settings for RecalculateConditionValues() + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + if (HasTag(Barotrauma.Tags.OxygenSource)) + { + conditionMultiplierCampaign *= campaign.Settings.OxygenMultiplier; + } + if (HasTag(Barotrauma.Tags.Fuel)) + { + conditionMultiplierCampaign *= campaign.Settings.FuelMultiplier; + } + } + if (!HasBeenInstantiatedOnce) + { + // This only needs to be done on the very first instantiation. + // MaxCondition will be multiplied in RecalculateConditionValues(), ensuring + // that Condition will stay in line with the multiplier from then on. + condition *= conditionMultiplierCampaign; + } + RecalculateConditionValues(); if (callOnItemLoaded) @@ -1276,6 +1336,7 @@ namespace Barotrauma #if CLIENT Submarine.ForceVisibilityRecheck(); #endif + HasBeenInstantiatedOnce = true; // Enable executing certain things only once } partial void InitProjSpecific(); @@ -1314,17 +1375,17 @@ namespace Barotrauma } //clone requireditem identifiers - foreach (var kvp in components[i].requiredItems) + foreach (var kvp in components[i].RequiredItems) { for (int j = 0; j < kvp.Value.Count; j++) { - if (!clone.components[i].requiredItems.ContainsKey(kvp.Key) || - clone.components[i].requiredItems[kvp.Key].Count <= j) + if (!clone.components[i].RequiredItems.ContainsKey(kvp.Key) || + clone.components[i].RequiredItems[kvp.Key].Count <= j) { continue; } - clone.components[i].requiredItems[kvp.Key][j].JoinedIdentifiers = + clone.components[i].RequiredItems[kvp.Key][j].JoinedIdentifiers = kvp.Value[j].JoinedIdentifiers; } } @@ -1363,20 +1424,17 @@ namespace Barotrauma } } - if (clonedContainedItems.Any()) + for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - for (int i = 0; i < components.Count && i < clone.components.Count; i++) + ItemComponent component = components[i], + cloneComp = clone.components[i]; + + if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox) { - ItemComponent component = components[i], - cloneComp = clone.components[i]; - - if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox) - { - continue; - } - - cloneBox.CloneFrom(origBox, clonedContainedItems); + continue; } + + cloneBox.CloneFrom(origBox, clonedContainedItems); } clone.FullyInitialized = true; @@ -1795,6 +1853,12 @@ namespace Barotrauma if (tags.Contains(tag)) { return; } tags.Add(tag); } + + public void RemoveTag(Identifier tag) + { + if (!tags.Contains(tag)) { return; } + tags.Remove(tag); + } public bool HasTag(Identifier tag) { @@ -1842,6 +1906,22 @@ namespace Barotrauma public bool ConditionalMatches(PropertyConditional conditional) { + return ConditionalMatches(conditional, checkContainer: true); + } + + public bool ConditionalMatches(PropertyConditional conditional, bool checkContainer) + { + if (checkContainer) + { + if (conditional.TargetContainer) + { + if (conditional.TargetGrandParent) + { + return container?.container != null && container.container.ConditionalMatches(conditional, checkContainer: false); + } + return container != null && container.ConditionalMatches(conditional, checkContainer: false); + } + } if (string.IsNullOrEmpty(conditional.TargetItemComponent)) { if (!conditional.Matches(this)) { return false; } @@ -1857,13 +1937,16 @@ namespace Barotrauma return true; } + /// + /// Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that should be handled by the code calling the method. + /// public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, Vector2? worldPosition = null) { if (!hasStatusEffectsOfType[(int)type]) { return; } foreach (StatusEffect effect in statusEffectLists[type]) { - ApplyStatusEffect(effect, type, deltaTime, character, limb, useTarget, isNetworkEvent, false, worldPosition); + ApplyStatusEffect(effect, type, deltaTime, character, limb, useTarget, isNetworkEvent, checkCondition: false, worldPosition); } } @@ -2102,7 +2185,7 @@ namespace Barotrauma /// public void RecalculateConditionValues() { - MaxCondition = Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); + MaxCondition = Prefab.Health * healthMultiplier * conditionMultiplierCampaign * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); IsFullCondition = MathUtils.NearlyEqual(Condition, MaxCondition); ConditionPercentage = MathUtils.Percentage(Condition, MaxCondition); } @@ -2196,7 +2279,9 @@ namespace Barotrauma { ItemComponent ic = updateableComponents[i]; - if (ic.IsActiveConditionals != null) + bool isParentInActive = ic.InheritParentIsActive && ic.Parent is { IsActive: false }; + + if (ic.IsActiveConditionals != null && !isParentInActive) { if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And) { @@ -2291,7 +2376,7 @@ namespace Barotrauma if (needsWaterCheck) { bool wasInWater = inWater; - inWater = !inWaterProofContainer && IsInWater() && !WaterProof; + inWater = !inWaterProofContainer && IsInWater(); if (inWater) { //the item has gone through the surface of the water @@ -2496,9 +2581,12 @@ namespace Barotrauma OnCollisionProjSpecific(impact); if (GameMain.NetworkMember is { IsClient: true }) { return; } - if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance) + if (ImpactTolerance > 0.0f && Math.Abs(impact) > ImpactTolerance && hasStatusEffectsOfType[(int)ActionType.OnImpact]) { - ApplyStatusEffects(ActionType.OnImpact, 1.0f); + foreach (StatusEffect effect in statusEffectLists[ActionType.OnImpact]) + { + ApplyStatusEffect(effect, ActionType.OnImpact, deltaTime: 1.0f); + } #if SERVER GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact)); #endif @@ -3141,7 +3229,12 @@ namespace Barotrauma if (character.IsDead) { return; } if (!UseInHealthInterface) { return; } - GameAnalyticsManager.AddDesignEvent("ApplyTreatment:" + Prefab.Identifier); + if (Prefab.ContentPackage == ContentPackageManager.VanillaCorePackage && + /* we don't need info of every item, we can get a good sample size just by logging 5% */ + Rand.Range(0.0f, 1.0f) < 0.05f) + { + GameAnalyticsManager.AddDesignEvent("ApplyTreatment:" + Prefab.Identifier); + } #if CLIENT if (user == Character.Controlled) { @@ -3559,12 +3652,12 @@ namespace Barotrauma } bool canAccess = false; - if (Container?.GetComponent() != null && + if (Container?.GetComponent() is { } cb && Container.CanClientAccess(sender)) { //items inside circuit boxes are inaccessible by "normal" means, //but the properties can still be edited through the circuit box UI - canAccess = true; + canAccess = !cb.Locked; } else { @@ -3881,6 +3974,8 @@ namespace Barotrauma } } + if (element.GetAttributeBool("markedfordeconstruction", false)) { deconstructItems.Add(item); } + float prevRotation = item.Rotation; if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); } if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); } @@ -3994,7 +4089,8 @@ namespace Barotrauma element.Add( new XAttribute("name", Prefab.OriginalName), new XAttribute("identifier", Prefab.Identifier), - new XAttribute("ID", ID)); + new XAttribute("ID", ID), + new XAttribute("markedfordeconstruction", deconstructItems.Contains(this))); if (PendingItemSwap != null) { @@ -4202,6 +4298,7 @@ namespace Barotrauma repairableItems.Remove(this); sonarVisibleItems.Remove(this); cleanableItems.Remove(this); + deconstructItems.Remove(this); RemoveFromDroppedStack(allowClientExecute: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 0770ba69f..7d5068b22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -1009,8 +1009,8 @@ namespace Barotrauma.MapCreatures.Behavior Body branchBody = GameMain.World.CreateRectangle(ConvertUnits.ToSimUnits(rect.Width * scale), ConvertUnits.ToSimUnits(rect.Height * scale), 1.5f); branchBody.BodyType = BodyType.Static; branchBody.UserData = branch; - branchBody.SetCollidesWith(Physics.CollisionRepair); - branchBody.SetCollisionCategories(Physics.CollisionRepair); + branchBody.SetCollidesWith(Physics.CollisionRepairableWall); + branchBody.SetCollisionCategories(Physics.CollisionRepairableWall); branchBody.Position = ConvertUnits.ToSimUnits(pos); branchBody.Enabled = HasBrokenThrough; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index b1f0392c8..4bdd1678f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -54,6 +54,9 @@ namespace Barotrauma public AITarget AiTarget => aiTarget; + /// + /// Indetectable characters can't be spotted by AIs and aren't visible on the sonar or health scanner. + /// public bool InDetectable { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 3fab10249..241300f15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -35,7 +35,7 @@ namespace Barotrauma /// 10% of the range if showEffects is true, 0 otherwise. /// /// - private readonly float cameraShake; + public float CameraShake { get; set; } /// /// How far away does the camera shake effect reach. @@ -45,7 +45,7 @@ namespace Barotrauma /// Same as attack range if showEffects is true, 0 otherwise. /// /// - private readonly float cameraShakeRange; + public float CameraShakeRange { get; set; } /// /// Color tint to apply to the player's screen when in range of the explosion. @@ -173,6 +173,11 @@ namespace Barotrauma /// public bool OnlyOutside; + /// + /// Should the normal damage sounds be played when the explosion damages something. Usually disabled. + /// + public bool PlayDamageSounds; + /// /// How much the explosion repairs items. /// @@ -239,6 +244,8 @@ namespace Barotrauma if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow); + PlayDamageSounds = element.GetAttributeBool(nameof(PlayDamageSounds), false); + EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f); @@ -247,8 +254,8 @@ namespace Barotrauma decal = element.GetAttributeString("decal", ""); decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); - cameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f); - cameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f); + CameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f); + CameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f); screenColorRange = element.GetAttributeFloat("screencolorrange", showEffects ? Attack.Range * 0.1f : 0f); screenColor = element.GetAttributeColor("screencolor", Color.Transparent); @@ -301,7 +308,7 @@ namespace Barotrauma Vector2 cameraPos = GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; - GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f); + GameMain.GameScreen.Cam.Shake = CameraShake * Math.Max((CameraShakeRange - cameraDist) / CameraShakeRange, 0.0f); #if CLIENT if (screenColor != Color.Transparent) { @@ -318,9 +325,12 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f)) { - RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, - IgnoredSubmarines, + RangedStructureDamage(worldPosition, displayRange, + Attack.GetStructureDamage(1.0f), + Attack.GetLevelWallDamage(1.0f), + attacker, IgnoredSubmarines, Attack.EmitStructureDamageParticles, + Attack.CreateWallDamageProjectiles, DistanceFalloff); } @@ -455,6 +465,7 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { + if (attack.OnlyHumans && !c.IsHuman) { continue; } if (IgnoredCharacters.Contains(c)) { continue; } if (!c.Enabled || @@ -485,6 +496,8 @@ namespace Barotrauma Dictionary damages = new Dictionary(); List modifiedAfflictions = new List(); + Limb closestLimb = null; + float closestDistFactor = 0; foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; } @@ -511,6 +524,11 @@ namespace Barotrauma if (distFactor > 0) { distFactors.Add(limb, distFactor); + if (distFactor > closestDistFactor) + { + closestLimb = limb; + closestDistFactor = distFactor; + } } } @@ -558,7 +576,11 @@ namespace Barotrauma //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 dir = worldPosition - limb.WorldPosition; Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; - AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); + + //only play the damage sound on the closest limb (playing it on all just sounds like a mess) + bool playSound = PlayDamageSounds && limb == closestLimb; + + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, playSound: playSound, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); damages.Add(limb, attackResult.Damage); } } @@ -622,7 +644,9 @@ namespace Barotrauma /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, - bool emitWallDamageParticles = true, bool distanceFalloff = true) + bool emitWallDamageParticles = true, + bool createWallDamageProjectiles = false, + bool distanceFalloff = true) { float dist = 600.0f; damagedStructures.Clear(); @@ -642,7 +666,7 @@ namespace Barotrauma 1.0f; if (distFactor <= 0.0f) { continue; } - structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); + structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles, createWallDamageProjectiles); if (damagedStructures.ContainsKey(structure)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index f4e2f9961..b30365a46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -855,9 +855,9 @@ namespace Barotrauma Gap g = new Gap(rect, isHorizontal, submarine, id: idRemap.GetOffsetId(element)) { linkedToID = new List(), + Layer = element.GetAttributeString(nameof(Layer), null) }; - - g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame).ToLower(), g.HiddenInGame); + g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame), g.HiddenInGame); return g; } @@ -868,7 +868,8 @@ namespace Barotrauma element.Add( new XAttribute("ID", ID), new XAttribute("horizontal", IsHorizontal ? "true" : "false"), - new XAttribute(nameof(HiddenInGame).ToLower(), HiddenInGame)); + new XAttribute(nameof(HiddenInGame), HiddenInGame), + new XAttribute(nameof(Layer), Layer ?? string.Empty)); element.Add(new XAttribute("rect", (int)(rect.X - Submarine.HiddenSubPosition.X) + "," + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 049d7f621..2899a06b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1141,8 +1141,10 @@ namespace Barotrauma float distanceMultiplier = 1; if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { - //gap blocked if the door is not open or the predicted state is not open - if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + //gap blocked if the door is closed, and we haven't made any predictions of it opening client-side + if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.PredictedState.HasValue) || + //OR we've predicted that the door is closed client-side + (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs index bf28c09aa..b25213a23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; +using System; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 9746cb9ae..3d85f1b53 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -17,7 +17,12 @@ namespace Barotrauma private readonly XElement configElement; - public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities; + public readonly record struct DisplayEntity( + Identifier Identifier, + Rectangle Rect, + float RotationRad); + + public readonly ImmutableArray DisplayEntities; public readonly Rectangle Bounds; @@ -81,23 +86,25 @@ namespace Barotrauma int minX = int.MaxValue, minY = int.MaxValue; int maxX = int.MinValue, maxY = int.MinValue; - var displayEntities = new List<(Identifier, Rectangle)>(); + var displayEntities = new List(); foreach (XElement entityElement in element.Elements()) { ushort id = (ushort)entityElement.GetAttributeInt("ID", 0); if (id > 0 && containedItemIDs.Contains(id)) { continue; } + if (entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) { continue; } + Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant()); - Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty); - if (!entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) - { - if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { displayEntities.Add((identifier, rect)); } - minX = Math.Min(minX, rect.X); - minY = Math.Min(minY, rect.Y - rect.Height); - maxX = Math.Max(maxX, rect.Right); - maxY = Math.Max(maxY, rect.Y); + float rotation = MathHelper.ToRadians(entityElement.GetAttributeFloat("rotation", 0.0f)); + if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) + { + displayEntities.Add(new DisplayEntity(identifier, rect, rotation)); } + minX = Math.Min(minX, rect.X); + minY = Math.Min(minY, rect.Y - rect.Height); + maxX = Math.Max(maxX, rect.Right); + maxY = Math.Max(maxY, rect.Y); } DisplayEntities = displayEntities.ToImmutableArray(); @@ -121,7 +128,11 @@ namespace Barotrauma public List CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false) { - return PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance); + var retVal = PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance); +#if CLIENT + GameMain.SubEditorScreen?.ReconstructLayers(); +#endif + return retVal; } public static List PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index 4731ca9a0..cd6bcf0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -2,6 +2,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; using Voronoi2; namespace Barotrauma @@ -75,6 +76,7 @@ namespace Barotrauma public void AddDamage(float damage, Vector2 worldPosition) { AddDamageProjSpecific(damage, worldPosition); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (Destroyed) { return; } if (!MathUtils.NearlyEqual(damage, 0.0f)) { NetworkUpdatePending = true; } @@ -100,6 +102,7 @@ namespace Barotrauma #if CLIENT SoundPlayer.PlaySound("icebreak", WorldPosition); #endif + Vector2 center = Vector2.Zero; //generate initial triangles (one triangle from each edge to the center of the cell) List> triangles = new List>(); foreach (var cell in Cells) @@ -114,6 +117,11 @@ namespace Barotrauma }; triangles.Add(triangleVerts); } + center += cell.Center; + } + if (Cells.Any()) + { + center /= Cells.Count; } //split triangles that have edges more than 1000 units long @@ -174,23 +182,24 @@ namespace Barotrauma Vector2 bodyDiff = simTriangleCenter - Body.Position; fragment.Body.LinearVelocity = (bodyDiff + Rand.Vector(0.5f)).ClampLength(15.0f); - fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f);// MathHelper.Clamp(-bodyDiff.X * 0.1f, -0.5f, 0.5f); + fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f); Level.Loaded.UnsyncedExtraWalls.Add(fragment); #if CLIENT - for (int i = 0; i < 20; i++) + for (int i = 0; i < 5; i++) { int startEdgeIndex = Rand.Int(3); Vector2 pos1 = triangle[startEdgeIndex]; Vector2 pos2 = triangle[(startEdgeIndex + 1) % 3]; - var particle = GameMain.ParticleManager.CreateParticle("iceshards", + var particle = GameMain.ParticleManager.CreateParticle("iceexplosion", triangleCenter + Vector2.Lerp(pos1, pos2, Rand.Range(0.0f, 1.0f)), - Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f); + velocity: (Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f)); if (particle != null) { particle.Size *= Rand.Range(1.0f, 5.0f); + particle.ColorMultiplier *= Rand.Range(0.7f, 1.0f); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 592e70844..cea585ed9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -119,7 +119,6 @@ namespace Barotrauma /// /// Caves, ruins, outposts and similar enclosed areas /// - /// public bool IsEnclosedArea() { return @@ -127,6 +126,7 @@ namespace Barotrauma PositionType == PositionType.Ruin || PositionType == PositionType.Outpost || PositionType == PositionType.BeaconStation || + PositionType == PositionType.Wreck || PositionType == PositionType.AbyssCave; } } @@ -698,106 +698,8 @@ namespace Barotrauma //---------------------------------------------------------------------------------- //generate voronoi sites //---------------------------------------------------------------------------------- - - Point siteInterval = GenerationParams.VoronoiSiteInterval; - int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y); - Point siteVariance = GenerationParams.VoronoiSiteVariance; - siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); - siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); - const int caveSiteInterval = 500; - for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) - { - for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) - { - int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient); - int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient); - bool closeToTunnel = false; - bool closeToCave = false; - foreach (Tunnel tunnel in Tunnels) - { - float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); - for (int i = 1; i < tunnel.Nodes.Count; i++) - { - if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; } - if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; } - if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; } - if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; } - - double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); - if (Math.Sqrt(tunnelDistSqr) < minDist) - { - closeToTunnel = true; - //tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); - if (tunnel.Type == TunnelType.Cave) - { - closeToCave = true; - } - break; - } - } - } - - if (!closeToTunnel) - { - //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry - if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } - } - - if (!TooCloseToOtherSites(siteX, siteY)) - { - siteCoordsX.Add(siteX); - siteCoordsY.Add(siteY); - } - - if (closeToCave) - { - for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval) - { - for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) - { - int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); - int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); - - if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval)) - { - siteCoordsX.Add(caveSiteX); - siteCoordsY.Add(caveSiteY); - } - } - } - } - } - } - - bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f) - { - float minDistanceSqr = minDistance * minDistance; - for (int i = 0; i < siteCoordsX.Count; i++) - { - if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr) - { - return true; - } - } - return false; - } - - for (int i = 0; i < siteCoordsX.Count; i++) - { - Debug.Assert( - siteCoordsX[i] > 0 || siteCoordsY[i] > 0, - $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); - Debug.Assert( - siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height, - $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); - for (int j = i + 1; j < siteCoordsX.Count; j++) - { - Debug.Assert( - MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 1.0f, - "Potential error in level generation: two voronoi sites are extremely close to each other."); - } - } + GenerateVoronoiSites(); GenerateEqualityCheckValue(LevelGenStage.VoronoiGen); @@ -809,13 +711,52 @@ namespace Barotrauma sw2.Start(); Debug.Assert(siteCoordsX.Count == siteCoordsY.Count); - List graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height); - Debug.WriteLine("MakeVoronoiGraph: " + sw2.ElapsedMilliseconds + " ms"); - sw2.Restart(); - - //construct voronoi cells based on the graph edges - cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid); + int remainingRetries = 5; + bool voronoiGraphInvalid = false; + do + { + remainingRetries--; + voronoiGraphInvalid = false; + //construct voronoi cells based on the graph edges + List graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), borders.Width, borders.Height); + cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid); + for (int i = 0; i < cells.Count; i++) + { + for (int j = i + 1; j < cells.Count; j++) + { + //sites can never be inside multiple cells in a voronoi graph by definition + //if they are, that'll cause severe issues with the rest of the level generation. + + //There seems to be a very rare issue that sometimes causes the graph to generate incorrectly (see #10944 and #12980), + //leading to a crash due. I haven't been able to figure out what's causing that - there don't seem to be any issues in the sites, + //so I'm getting the feeling it could be a bug with the voronoi graph generation. + + //If that happens, let's just retry a couple of times (re-randomizing the sites and regenerating + //the map seems to fix the issue in all cases I've seen) + if (cells[j].IsPointInside(cells[i].Center)) + { + voronoiGraphInvalid = true; + break; + } + if (voronoiGraphInvalid) { break; } + } + } + if (voronoiGraphInvalid) + { + string errorMsg = "Unknown error during level generation. Invalid voronoi graph: the same voronoi site was inside multiple cells."; + if (remainingRetries > 0) + { + DebugConsole.AddWarning(errorMsg + " Retrying..."); + GenerateVoronoiSites(); + } + else + { + //throw a console error and let the generation finish, hoping for the best + DebugConsole.ThrowError(errorMsg); + } + } + } while (remainingRetries > 0 && voronoiGraphInvalid); GenerateAbyssGeometry(); GenerateAbyssPositions(); @@ -1449,6 +1390,116 @@ namespace Barotrauma Generating = false; } + + private void GenerateVoronoiSites() + { + Point siteInterval = GenerationParams.VoronoiSiteInterval; + int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y); + Point siteVariance = GenerationParams.VoronoiSiteVariance; + siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); + siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); + const int caveSiteInterval = 500; + for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) + { + for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) + { + int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient); + int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient); + + bool closeToTunnel = false; + bool closeToCave = false; + foreach (Tunnel tunnel in Tunnels) + { + float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; } + if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; } + if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; } + if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; } + + double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + if (Math.Sqrt(tunnelDistSqr) < minDist) + { + closeToTunnel = true; + //tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + if (tunnel.Type == TunnelType.Cave) + { + closeToCave = true; + } + break; + } + } + } + + if (!closeToTunnel) + { + //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry + if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } + } + + if (!TooCloseToOtherSites(siteX, siteY)) + { + siteCoordsX.Add(siteX); + siteCoordsY.Add(siteY); + } + + if (closeToCave) + { + for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval) + { + for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) + { + int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); + int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); + + if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval)) + { + siteCoordsX.Add(caveSiteX); + siteCoordsY.Add(caveSiteY); + } + } + } + } + } + } + + bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f) + { + float minDistanceSqr = minDistance * minDistance; + for (int i = 0; i < siteCoordsX.Count; i++) + { + if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr) + { + return true; + } + } + return false; + } + + for (int i = 0; i < siteCoordsX.Count; i++) + { + Debug.Assert( + !double.IsNaN(siteCoordsX[i]) && !double.IsInfinity(siteCoordsX[i]), + $"Potential error in level generation: invalid voronoi site ({siteCoordsX[i]})"); + Debug.Assert( + !double.IsNaN(siteCoordsY[i]) && !double.IsInfinity(siteCoordsY[i]), + $"Potential error in level generation: invalid voronoi site ({siteCoordsY[i]})"); + Debug.Assert( + siteCoordsX[i] > 0 || siteCoordsY[i] > 0, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + Debug.Assert( + siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + for (int j = i + 1; j < siteCoordsX.Count; j++) + { + Debug.Assert( + MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 10.0f, + "Potential error in level generation: two voronoi sites are extremely close to each other."); + } + } + } + private List GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance) { List pathNodes = new List { startPosition }; @@ -3165,7 +3216,7 @@ namespace Barotrauma private List GetAllValidClusterLocations() { var subBorders = new List(); - Wrecks.ForEach(w => AddBordersToList(w)); + Wrecks.ForEach(AddBordersToList); AddBordersToList(BeaconStation); var locations = new List(); @@ -3186,6 +3237,8 @@ namespace Barotrauma { if (s == null) { return; } var rect = Submarine.AbsRect(s.WorldPosition, s.Borders.Size.ToVector2()); + // range of piezo crystal discharge is 3500, pad the rect to ensure no such kind of hazards spawn near + rect.Inflate(4000, 4000); subBorders.Add(rect); } @@ -3202,9 +3255,14 @@ namespace Barotrauma { foreach (var r in subBorders) { - if (Submarine.RectContains(r, e.Point1)) { return true; } - if (Submarine.RectContains(r, e.Point2)) { return true; } - if (Submarine.RectContains(r, eCenter)) { return true; } + if (r.Contains(e.Point1)) { return true; } + if (r.Contains(e.Point2)) { return true; } + if (r.Contains(eCenter)) { return true; } + + if (MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, r, out _)) + { + return true; + } } return false; } @@ -3291,7 +3349,7 @@ namespace Barotrauma Vector2 endPos = startPos - Vector2.UnitY * Size.Y; //try to find a level wall below the position unless the position is indoors - if (!potentialPos.IsEnclosedArea()) + if (!potentialPos.PositionType.IsEnclosedArea()) { if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), @@ -3684,7 +3742,12 @@ namespace Barotrauma var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom; // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. - Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000); + int padding = 1500; + Rectangle paddedBorders = new Rectangle( + subBorders.X - padding, + subBorders.Y + padding, + subBorders.Width + padding * 2, + subBorders.Height + padding * 2); var positions = new List(); var rects = new List(); @@ -3733,7 +3796,8 @@ namespace Barotrauma { if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance) { - hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient); + hull.WaterVolume = + Math.Max(hull.WaterVolume, hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient)); } } // Only spawn thalamus when the wreck has some thalamus items defined. @@ -3806,10 +3870,18 @@ namespace Barotrauma } } positions.Add(spawnPoint); - bool isBlocked = IsBlocked(spawnPoint, subBorders.Size - new Point(step + 50)); + //shrink the bounds a bit to allow the sub to go slightly inside the wall + //(just enough that it doesn't look like it's floating) + int shrinkAmount = step + 50; + Rectangle shrunkenBorders = new Rectangle( + subBorders.X + shrinkAmount, + subBorders.Y - shrinkAmount, + subBorders.Width - shrinkAmount * 2, + subBorders.Height - shrinkAmount * 2); + bool isBlocked = IsBlocked(spawnPoint, shrunkenBorders); if (isBlocked) { - rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size)); + rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint() + subBorders.Location, subBorders.Size)); Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls."); } else if (!bottomFound) @@ -3832,7 +3904,8 @@ namespace Barotrauma float maxMovement = 5000; float totalAmount = 0; bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); - while (!IsSideBlocked(subBorders, amount > 0)) + //move until the side is no longer blocked + while (IsSideBlocked(subBorders, front: amount < 0)) { foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); totalAmount += amount; @@ -3854,7 +3927,7 @@ namespace Barotrauma { var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient); waypoints.Remove(wp); - if (!IsBlocked(wp.WorldPosition, paddedDimensions)) + if (!IsBlocked(wp.WorldPosition, paddedBorders)) { spawnPoint = wp.WorldPosition; return true; @@ -3867,76 +3940,73 @@ namespace Barotrauma { // Shoot five rays and pick the highest hit point. int rayCount = 5; - var positions = new Vector2[rayCount]; + var hitPositions = new Vector2[rayCount]; bool hit = false; for (int i = 0; i < rayCount; i++) { - float quarterWidth = subBorders.Width * 0.25f; - Vector2 rayStart = spawnPoint; - switch (i) - { - case 1: - rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y); - break; - case 2: - rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y); - break; - case 3: - rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y); - break; - case 4: - rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y); - break; - } + //cast rays starting from the left side of the sub, offset by 20% to 80% of the sub's width + //(ignoring the very back and front of the sub, it's fine if they overlap a bit) + float xOffset = + subBorders.Width * MathHelper.Lerp(0.2f, 0.8f, i / (float)(rayCount - 1)); + Vector2 rayStart = new Vector2( + spawnPoint.X + subBorders.Location.X + xOffset, + spawnPoint.Y); var simPos = ConvertUnits.ToSimUnits(rayStart); var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1), customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); if (body != null) { - positions[i] = - ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + - new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1)); + hitPositions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition); hit = true; } } - float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y); - spawnPoint = new Vector2(spawnPoint.X, highestPoint); + int dir = placement == PlacementType.Bottom ? -1 : 1; + float highestPoint = placement == PlacementType.Bottom ? hitPositions.Max(p => p.Y) : hitPositions.Min(p => p.Y); + float halfHeight = subBorders.Height / 2; + float centerOffset = subBorders.Y - halfHeight; + spawnPoint = new Vector2(spawnPoint.X, highestPoint + halfHeight * -dir - centerOffset); return hit; } bool IsSideBlocked(Rectangle subBorders, bool front) { + Point centerOffset = new Point(subBorders.Center.X, subBorders.Y - subBorders.Height / 2); + // Shoot three rays and check whether any of them hits. int rayCount = 3; + int dir = front ? 1 : -1; Vector2 halfSize = subBorders.Size.ToVector2() / 2; Vector2 quarterSize = halfSize / 2; - var positions = new Vector2[rayCount]; for (int i = 0; i < rayCount; i++) { - float dir = front ? 1 : -1; - Vector2 rayStart; - Vector2 to; + Vector2 rayStart, to; switch (i) { case 1: - rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y); - to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); - break; case 2: - rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y); - to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + float yOffset = quarterSize.Y * (i == 1 ? 1 : -1); + //from a position half-way towards the edge, to the edge. + //we start half-way towards the edge instead of the center, because we want to allow things to poke partially inside the sub + rayStart = new Vector2(spawnPoint.X + quarterSize.X * dir, spawnPoint.Y + yOffset); + to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); break; case 0: default: + //center to center-right rayStart = spawnPoint; to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); break; } + + rayStart += centerOffset.ToVector2(); + to += centerOffset.ToVector2(); + Vector2 simPos = ConvertUnits.ToSimUnits(rayStart); if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to), customPredicate: f => f.Body?.UserData is VoronoiCell cell, - collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, + allowInsideFixture: true) != null) { return true; } @@ -3944,10 +4014,11 @@ namespace Barotrauma return false; } - bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1) + bool IsBlocked(Vector2 pos, Rectangle submarineBounds) { - float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared(); - Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size); + Rectangle bounds = new Rectangle( + (int)pos.X + submarineBounds.X, (int)pos.Y + submarineBounds.Y, + submarineBounds.Width, submarineBounds.Height); if (Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds))) { return true; @@ -3958,13 +4029,16 @@ namespace Barotrauma { return true; } - return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v))); + return cells.Any(c => + c.Body != null && + c.BodyVertices.Any(v => bounds.ContainsWorld(v))); } } // For debugging private readonly Dictionary> wreckPositions = new Dictionary>(); private readonly Dictionary> blockedRects = new Dictionary>(); + private void CreateWrecks() { var totalSW = new Stopwatch(); @@ -4005,6 +4079,18 @@ namespace Barotrauma wreckCount = Math.Max(wreckCount, 1); } + if (LevelData.ForceWreck != null) + { + //force the desired wreck to be chosen first + var matchingFile = wreckFiles.FirstOrDefault(w => w.Path == LevelData.ForceWreck.FilePath); + if (matchingFile != null) + { + wreckFiles.Remove(matchingFile); + wreckFiles.Insert(0, matchingFile); + } + wreckCount = Math.Max(wreckCount, 1); + } + Wrecks = new List(wreckCount); for (int i = 0; i < wreckCount; i++) { @@ -4286,7 +4372,7 @@ namespace Barotrauma private void CreateBeaconStation() { - if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; } + if (!LevelData.HasBeaconStation && LevelData.ForceBeaconStation == null && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; } var beaconStationFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) .OrderBy(f => f.UintIdentifier).ToList(); @@ -4307,6 +4393,10 @@ namespace Barotrauma DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); } } + else if (LevelData.ForceBeaconStation != null) + { + contentFile = beaconStationFiles.FirstOrDefault(b => b.Path == LevelData.ForceBeaconStation.FilePath); + } if (contentFile == null) { @@ -4384,10 +4474,17 @@ namespace Barotrauma fuelPrefab, reactorContainer.Inventory, onSpawned: (it) => reactorComponent.PowerUpImmediately()); } - beaconSonar.CurrentMode = Sonar.Mode.Active; + if (beaconSonar == null) + { + DebugConsole.AddWarning($"Beacon station \"{BeaconStation.Info.Name}\" has no sonar. Beacon missions might not work correctly with this beacon station."); + } + else + { + beaconSonar.CurrentMode = Sonar.Mode.Active; #if SERVER - beaconSonar.Item.CreateServerEvent(beaconSonar); + beaconSonar.Item.CreateServerEvent(beaconSonar); #endif + } } else if (GameMain.NetworkMember is not { IsClient: true }) { @@ -4470,7 +4567,7 @@ namespace Barotrauma { foreach (var connectedSub in parentSub.GetConnectedSubs()) { - connectedSub.RealWorldCrushDepth = Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000); + connectedSub.SetCrushDepth(Math.Max(connectedSub.RealWorldCrushDepth, GetRealWorldDepth(0) + 1000)); } } @@ -4631,7 +4728,7 @@ namespace Barotrauma } /// - /// Calculate the "real" depth in meters from the surface of Europa + /// Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav terminal) /// public float GetRealWorldDepth(float worldPositionY) { @@ -4733,4 +4830,30 @@ namespace Barotrauma Loaded = null; } } + + static class PositionTypeExtensions + { + /// + /// Caves, ruins, outposts and similar enclosed areas + /// + public static bool IsEnclosedArea(this Level.PositionType positionType) + { + return + positionType == Level.PositionType.Cave || + positionType == Level.PositionType.AbyssCave || + positionType.IsIndoorsArea(); + } + + /// + /// Area inside a submarine (outpost, wreck, beacon station) + /// + public static bool IsIndoorsArea(this Level.PositionType positionType) + { + return + positionType == Level.PositionType.Outpost || + positionType == Level.PositionType.BeaconStation || + positionType == Level.PositionType.Ruin || + positionType == Level.PositionType.Wreck; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 25e106aba..fb6e9d1bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -43,6 +43,10 @@ namespace Barotrauma public OutpostGenerationParams ForceOutpostGenerationParams; + public SubmarineInfo ForceBeaconStation; + + public SubmarineInfo ForceWreck; + public bool AllowInvalidOutpost; public readonly Point Size; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 78deb6fe1..d7c70391f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -405,25 +405,35 @@ namespace Barotrauma } } - public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture otherFixture, Entity separatingEntity) + /// + /// Checks whether any fixture of the trigger body is in contact with any fixture belonging to the physics bodies of separatingEntity + /// + /// Physics body of the trigger + /// Fixture that got separated from the trigger + /// Entity that got separated from the trigger + /// + public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture separatingFixture, Entity separatingEntity) { //check if there are contacts with any other fixture of the trigger //(the OnSeparation callback happens when two fixtures separate, //e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body) - foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) + foreach (Fixture triggerFixture in triggerBody.FarseerBody.FixtureList) { - ContactEdge contactEdge = fixture.Body.ContactList; + ContactEdge contactEdge = triggerFixture.Body.ContactList; while (contactEdge != null) { if (contactEdge.Contact != null && contactEdge.Contact.Enabled && contactEdge.Contact.IsTouching) { - if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture) - { - var otherEntity = GetEntity(contactEdge.Contact.FixtureB == otherFixture ? + //which fixture of this contact belongs to the "other" body (not the trigger itself) + Fixture otherFixture = + contactEdge.Contact.FixtureA == triggerFixture ? contactEdge.Contact.FixtureB : - contactEdge.Contact.FixtureA); + contactEdge.Contact.FixtureA; + if (otherFixture != separatingFixture) + { + var otherEntity = GetEntity(otherFixture); if (otherEntity == separatingEntity) { return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7a0ef7607..dd3df3d5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -307,6 +307,12 @@ namespace Barotrauma // Adjust by current reputation price *= GetReputationModifier(true); + // Adjust by campaign difficulty settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + price *= campaign.Settings.ShopPriceMultiplier; + } + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { @@ -768,27 +774,22 @@ namespace Barotrauma else { DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}"); - DisplayName = - Type.NameFormats == null || !Type.NameFormats.Any() ? - TextManager.Get(nameIdentifier) : + DisplayName = + Type.NameFormats == null || !Type.NameFormats.Any() ? + TextManager.Get(nameIdentifier) : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value); } + TryAssignFactionBasedOnLocationType(campaign); if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - if (Faction == null) - { - Faction = campaign.GetRandomFaction(Rand.RandSync.Unsynced); - } - if (SecondaryFaction == null) - { - SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); - } + if (Type.Faction == Identifier.Empty) { Faction ??= campaign.GetRandomFaction(Rand.RandSync.Unsynced); } + if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); } } else { - Faction = null; - SecondaryFaction = null; + if (Type.Faction == Identifier.Empty) { Faction = null; } + if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; } } UnlockInitialMissions(Rand.RandSync.Unsynced); @@ -799,6 +800,30 @@ namespace Barotrauma } } + public void TryAssignFactionBasedOnLocationType(CampaignMode campaign) + { + if (campaign == null) { return; } + if (Type.Faction != Identifier.Empty) + { + Faction = Type.Faction == "None" ? null : TryFindFaction(Type.Faction); + } + if (Type.SecondaryFaction != Identifier.Empty) + { + SecondaryFaction = Type.SecondaryFaction == "None" ? null : TryFindFaction(Type.SecondaryFaction); + } + + Faction TryFindFaction(Identifier identifier) + { + var faction = campaign.GetFaction(identifier); + if (faction == null) + { + DebugConsole.ThrowError($"Error in location type \"{Type.Identifier}\": failed to find a faction with the identifier \"{identifier}\".", + contentPackage: Type.ContentPackage); + } + return faction; + } + } + public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { if (Type.MissionIdentifiers.Any()) @@ -1127,6 +1152,7 @@ namespace Barotrauma nameIdentifier = type.GetRandomNameId(rand, existingLocations); if (nameIdentifier.IsEmpty) { + //backwards compatibility rawName = type.GetRandomRawName(rand, existingLocations); if (rawName.IsNullOrEmpty()) { @@ -1134,7 +1160,15 @@ namespace Barotrauma rawName = "none"; } nameIdentifier = rawName.ToIdentifier(); - DisplayName = rawName; + if (type.NameFormats == null || !type.NameFormats.Any()) + { + DisplayName = rawName; + } + else + { + nameFormatIndex = rand.Next() % type.NameFormats.Count; + DisplayName = type.NameFormats[nameFormatIndex].Replace("[name]", rawName); + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index b300c4d02..d67c0c29d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -86,6 +86,16 @@ namespace Barotrauma public Identifier ReplaceInRadiation { get; } + /// + /// If set, forces the location to be assigned to this faction. Set to "None" if you don't want the location to be assigned to any faction. + /// + public Identifier Faction { get; } + + /// + /// If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want the location to be assigned to any secondary faction. + /// + public Identifier SecondaryFaction { get; } + public Sprite Sprite { get; private set; } public Sprite RadiationSprite { get; } @@ -134,6 +144,9 @@ namespace Barotrauma AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true); AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); + Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); + SecondaryFaction = element.GetAttributeIdentifier(nameof(SecondaryFaction), Identifier.Empty); + ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 1934e9c69..5e2b376b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -298,7 +298,7 @@ namespace Barotrauma //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost if (CurrentLocation == null) { - FindStartLocation(l => l.Type.HasOutpost); + FindStartLocation(l => l.Type.HasOutpost && l.Type.OutpostTeam == CharacterTeamType.FriendlyNPC); } void FindStartLocation(Func predicate) @@ -313,25 +313,38 @@ namespace Barotrauma } } - StartLocation.SecondaryFaction = null; + StartLocation.SecondaryFaction = null; var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost); if (startOutpostFaction != null) { StartLocation.Faction = startOutpostFaction; - foreach (var connection in StartLocation.Connections) + } + foreach (var connection in StartLocation.Connections) + { + //force locations adjacent to the start location to have an outpost + //non-inhabited locations seem to be confusing to new players, particularly + //on the first round/mission when they still don't know how transitions between levels work + var otherLocation = connection.OtherLocation(StartLocation); + if (!otherLocation.HasOutpost()) { - var otherLocation = connection.OtherLocation(StartLocation); - if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + if (LocationType.Prefabs.TryGet("outpost".ToIdentifier(), out LocationType outpostLocationType)) { - otherLocation.Faction = startOutpostFaction; + otherLocation.ChangeType(campaign, outpostLocationType); } } + + if (otherLocation.HasOutpost() && + otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC && + otherLocation.Type.Faction.IsEmpty) + { + otherLocation.Faction = startOutpostFaction; + } } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0); - if (loops == 0 && (campaign.Settings.Difficulty == GameDifficulty.Easy || campaign.Settings.Difficulty == GameDifficulty.Medium)) + if (loops == 0 && (campaign.Settings.WorldHostility == WorldHostilityOption.Low || campaign.Settings.WorldHostility == WorldHostilityOption.Medium)) { if (StartLocation != null) { @@ -705,10 +718,19 @@ namespace Barotrauma foreach (Location location in Locations) { location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome)); + location.TryAssignFactionBasedOnLocationType(campaign); if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); - location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + if (location.Type.Faction.IsEmpty) + { + //no faction defined in the location type, assign a random one + location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + } + if (location.Type.SecondaryFaction.IsEmpty) + { + //no secondary faction defined in the location type, assign a random one + location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } } location.CreateStores(force: true); } @@ -1502,6 +1524,10 @@ namespace Barotrauma LocationType prevLocationType = location.Type; LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); location.ChangeType(campaign, newLocationType); + + var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty); + location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + if (showNotifications && prevLocationType != location.Type) { var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier); @@ -1512,9 +1538,6 @@ namespace Barotrauma } } - var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty); - location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); - var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 65ace1b9c..74ff16785 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -651,10 +651,24 @@ namespace Barotrauma Item.UpdatePendingConditionUpdates(deltaTime); if (mapEntityUpdateTick % MapEntityUpdateInterval == 0) { - foreach (Item item in Item.ItemList) + Item lastUpdatedItem = null; + + try { - if (GameMain.LuaCs.Game.UpdatePriorityItems.Contains(item)) continue; - item.Update(deltaTime * MapEntityUpdateInterval, cam); + foreach (Item item in Item.ItemList) + { + if (GameMain.LuaCs.Game.UpdatePriorityItems.Contains(item)) { continue; } + lastUpdatedItem = item; + item.Update(deltaTime * MapEntityUpdateInterval, cam); + } + } + catch (InvalidOperationException e) + { + GameAnalyticsManager.AddErrorEventOnce( + "MapEntity.UpdateAll:ItemUpdateInvalidOperation", + GameAnalyticsManager.ErrorSeverity.Critical, + $"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}"); + throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 7bb55ccd6..18a0f177b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -131,6 +131,13 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the outpost generation parameters that should be used if this outpost has become critically irradiated."), Editable] public string ReplaceInRadiation { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "By default, sonar only shows the outline of the sub/outpost from the outside. Enable this if you want to see each structure individually."), Editable] + public bool AlwaysShowStructuresOnSonar + { + get; + set; + } + public ContentPath OutpostFilePath { get; set; } public class ModuleCount diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index ba0a62262..a7f726142 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -238,8 +238,7 @@ namespace Barotrauma foreach (Hull hull in Hull.HullList) { if (hull.Submarine != sub) { continue; } - if (string.IsNullOrEmpty(hull.RoomName) || - hull.RoomName.Contains("RoomName.", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(hull.RoomName)) { hull.RoomName = hull.CreateRoomName(); } @@ -877,16 +876,16 @@ namespace Barotrauma } } - if (availableModules.Count() == 0) { return null; } + if (!availableModules.Any()) { return null; } //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); + availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType)); //if not found, search for modules suitable for any location type if (!modulesSuitableForLocationType.Any()) { - modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + modulesSuitableForLocationType = availableModules.Where(m => m.OutpostModuleInfo.IsAllowedInAnyLocationType()); } if (!modulesSuitableForLocationType.Any()) @@ -956,11 +955,12 @@ namespace Barotrauma { if (disallowNonLocationTypeSpecific) { + //don't use OutpostModuleInfo.IsLocationTypeAllowed here - we're trying to choose a module specifically for this location type, not modules suitable for any location type suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); } else { - suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier) || !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType)); } } if (requireAllowAttachToPrevious && prevModule != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 9a9405020..46330d892 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System; using System.Collections.Generic; using System.Linq; @@ -132,6 +133,16 @@ namespace Barotrauma this.allowedLocationTypes.Add(locationType); } } + public bool IsAllowedInAnyLocationType() + { + return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier()); + } + + public bool IsAllowedInLocationType(LocationType locationType) + { + if (locationType == null || IsAllowedInAnyLocationType()) { return true; } + return allowedLocationTypes.Contains(locationType.Identifier); + } public void DetermineGapPositions(Submarine sub) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index cce8509ab..37e00e1b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -24,6 +24,8 @@ namespace Barotrauma public float damage; public Gap gap; + public bool NoPhysicsBody; + public Structure Wall { get; } public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this)); public Vector2 WorldPosition => Wall.SectionPosition(Wall.Sections.IndexOf(this), world: true); @@ -49,21 +51,18 @@ namespace Barotrauma public static List WallList = new List(); const float LeakThreshold = 0.1f; + const float BigGapThreshold = 0.7f; #if CLIENT public SpriteEffects SpriteEffects = SpriteEffects.None; #endif //dimensions of the wall sections' physics bodies (only used for debug rendering) - private readonly List bodyDebugDimensions = new List(); + private readonly Dictionary bodyDimensions = new Dictionary(); private static Explosion explosionOnBroken; -#if DEBUG [Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)] -#else - [Serialize(false, IsPropertySaveable.Yes)] -#endif public bool Indestructible { get; @@ -596,7 +595,7 @@ namespace Barotrauma private void CreateStairBodies() { Bodies = new List(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f)); @@ -621,7 +620,7 @@ namespace Barotrauma newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset * Scale; - bodyDebugDimensions.Add(new Vector2(bodyWidth, bodyHeight)); + bodyDimensions.Add(newBody, new Vector2(bodyWidth, bodyHeight)); Bodies.Add(newBody); } @@ -962,18 +961,22 @@ namespace Barotrauma return true; } - public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true) + public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true, bool createWallDamageProjectiles = false) { if (!Prefab.Body || Prefab.Platform || Indestructible) { return; } if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; } var section = Sections[sectionIndex]; - + float prevDamage = section.damage; + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + SetDamage(sectionIndex, section.damage + damage, attacker); + } #if CLIENT if (damage > 0 && emitParticles) { - float dmg = Math.Min(MaxHealth - section.damage, damage); + float dmg = Math.Min(section.damage - prevDamage, damage); float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f))); // Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle if (particleAmount < 1 && Rand.Value() < 0.10f) @@ -996,13 +999,13 @@ namespace Barotrauma var particle = GameMain.ParticleManager.CreateParticle(Prefab.DamageParticle, position: particlePosFinal, velocity: Rand.Vector(Rand.Range(1.0f, 50.0f)), collisionIgnoreTimer: 1f); - if (particle == null) break; + if (particle == null) { break; } } } #endif if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - SetDamage(sectionIndex, section.damage + damage, attacker); + SetDamage(sectionIndex, section.damage + damage, attacker, createWallDamageProjectiles: createWallDamageProjectiles); } } @@ -1132,7 +1135,7 @@ namespace Barotrauma if (MathUtils.CircleIntersectsRectangle(transformedPos, attack.DamageRange, sectionRect)) { damageAmount = attack.GetStructureDamage(deltaTime); - AddDamage(i, damageAmount, attacker); + AddDamage(i, damageAmount, attacker, createWallDamageProjectiles: attack.CreateWallDamageProjectiles); #if CLIENT if (attack.EmitStructureDamageParticles) { @@ -1165,7 +1168,11 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool isNetworkEvent = true, bool createExplosionEffect = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, + bool createNetworkEvent = true, + bool isNetworkEvent = true, + bool createExplosionEffect = true, + bool createWallDamageProjectiles = false) { if (Submarine != null && Submarine.GodMode || (Indestructible && !isNetworkEvent)) { return; } if (!Prefab.Body) { return; } @@ -1173,6 +1180,8 @@ namespace Barotrauma damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth); + if (Sections[sectionIndex].NoPhysicsBody) { return; } + #if SERVER if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage) { @@ -1301,13 +1310,22 @@ namespace Barotrauma } var gap = Sections[sectionIndex].gap; - float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); + float damageRatio = MaxHealth <= 0.0f ? 0 : damage / MaxHealth; + float gapOpen = 0; + if (damageRatio > BigGapThreshold) + { + gapOpen = MathHelper.Lerp(0.35f, 0.75f, MathUtils.InverseLerp(BigGapThreshold, 1.0f, damageRatio)); + } + else if (damageRatio > LeakThreshold) + { + gapOpen = MathHelper.Lerp(0f, 0.35f, MathUtils.InverseLerp(LeakThreshold, BigGapThreshold, damageRatio)); + } gap.Open = gapOpen; //gap appeared or became much larger -> explosion effect if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom) { - CreateWallDamageExplosion(gap, attacker); + CreateWallDamageExplosion(gap, attacker, createWallDamageProjectiles); } } @@ -1337,7 +1355,7 @@ namespace Barotrauma UpdateSections(); } - private static void CreateWallDamageExplosion(Gap gap, Character attacker) + private static void CreateWallDamageExplosion(Gap gap, Character attacker, bool createProjectiles) { const float explosionRange = 500.0f; float explosionStrength = gap.Open; @@ -1367,31 +1385,52 @@ namespace Barotrauma { explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null); } + explosionOnBroken.CameraShake = 5.0f; explosionOnBroken.IgnoreCover = false; explosionOnBroken.OnlyInside = true; explosionOnBroken.DistanceFalloff = false; + explosionOnBroken.PlayDamageSounds = true; explosionOnBroken.DisableParticles(); } - + explosionOnBroken.CameraShake = 25.0f; explosionOnBroken.IgnoredCover = gap.ConnectedWall?.ToEnumerable(); - explosionOnBroken.Attack.Range = explosionRange * gap.Open; + explosionOnBroken.Attack.Range = explosionOnBroken.CameraShakeRange = explosionRange * gap.Open; explosionOnBroken.Attack.DamageMultiplier = explosionStrength; explosionOnBroken.Attack.Stun = MathHelper.Clamp(explosionStrength, 0.5f, 1.0f); explosionOnBroken.IgnoredCharacters.Clear(); if (attacker?.AIController is EnemyAIController) { explosionOnBroken.IgnoredCharacters.Add(attacker); } explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker); + + if (createProjectiles) + { + if (ItemPrefab.Prefabs.TryGet("walldamageprojectile", out var projectilePrefab) && linkedHull != null) + { + float angle = gap.IsHorizontal ? + (linkedHull.WorldPosition.X < gap.WorldPosition.X ? MathHelper.Pi : 0) : + (linkedHull.WorldPosition.Y < gap.WorldPosition.Y ? -MathHelper.PiOver2 : MathHelper.PiOver2); + Spawner.AddItemToSpawnQueue(projectilePrefab, gap.WorldPosition, onSpawned: (item) => + { + item.body.SetTransformIgnoreContacts(item.body.SimPosition, angle); + var projectile = item.GetComponent(); + projectile?.Use(); + }); + } + } + #if CLIENT + SoundPlayer.PlaySound("Ricochet", gap.WorldPosition); if (linkedHull != null) { for (int i = 0; i <= 50; i++) { - Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y)); - var velocity = gap.IsHorizontal ? + var emitDirection = gap.IsHorizontal ? gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX : gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY; - velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f)); - var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f); - if (particle == null) { break; } + Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y)); + emitDirection = new Vector2(emitDirection.X + Rand.Range(-0.2f, 0.2f), emitDirection.Y + Rand.Range(-0.2f, 0.2f)); + var shrapnelParticle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, emitDirection * Rand.Range(100.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.1f); + var sparkParticle = GameMain.ParticleManager.CreateParticle("whitespark", particlePos, emitDirection * Rand.Range(1000.0f, 3000.0f), hullGuess: linkedHull, collisionIgnoreTimer: 0.05f); + if (shrapnelParticle == null || sparkParticle == null) { break; } } } #endif @@ -1416,7 +1455,7 @@ namespace Barotrauma GameMain.World.Remove(b); } Bodies.Clear(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); #if CLIENT convexHulls?.ForEach(ch => ch.Remove()); convexHulls?.Clear(); @@ -1454,8 +1493,27 @@ namespace Barotrauma if (hasHoles || !Bodies.Any()) { Body sensorBody = CreateRectBody(rect, createConvexHull: false); - sensorBody.CollisionCategories = Physics.CollisionRepair; - sensorBody.SetIsSensor(true); + sensorBody.CollisionCategories = Physics.CollisionRepairableWall; + } + + foreach (var section in Sections) + { + bool intersectsWithBody = false; + foreach (var body in Bodies) + { + var bodyRect = new Rectangle( + ConvertUnits.ToDisplayUnits(body.Position - bodyDimensions[body] / 2).ToPoint(), + ConvertUnits.ToDisplayUnits(bodyDimensions[body]).ToPoint()); + + Rectangle sectionRect = section.rect; + sectionRect.Y -= section.rect.Height; + if (bodyRect.Intersects(sectionRect)) + { + intersectsWithBody = true; + break; + } + } + section.NoPhysicsBody = !intersectsWithBody; } } @@ -1511,7 +1569,7 @@ namespace Barotrauma } Bodies.Add(newBody); - bodyDebugDimensions.Add(new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height))); + bodyDimensions.Add(newBody, new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height))); return newBody; } @@ -1534,7 +1592,7 @@ namespace Barotrauma StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left; Bodies.ForEach(b => GameMain.World.Remove(b)); Bodies.Clear(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); CreateStairBodies(); } @@ -1562,7 +1620,7 @@ namespace Barotrauma StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left; Bodies.ForEach(b => GameMain.World.Remove(b)); Bodies.Clear(); - bodyDebugDimensions.Clear(); + bodyDimensions.Clear(); CreateStairBodies(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index edce76390..be4caa5b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -188,7 +188,6 @@ namespace Barotrauma } return realWorldCrushDepth.Value; } - set { realWorldCrushDepth = value; } } /// @@ -976,7 +975,7 @@ namespace Barotrauma if (ignoreLevel && fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; } if (!fixture.CollisionCategories.HasFlag(Physics.CollisionLevel) && !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) - && !fixture.CollisionCategories.HasFlag(Physics.CollisionRepair)) { return -1; } + && !fixture.CollisionCategories.HasFlag(Physics.CollisionRepairableWall)) { return -1; } if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; } if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; } if (fixture.Body.UserData as string == "ruinroom") { return -1; } @@ -1113,27 +1112,45 @@ namespace Barotrauma } public void EnableFactionSpecificEntities(Identifier factionIdentifier) + { + foreach (var faction in FactionPrefab.Prefabs) + { + SetLayerEnabled(faction.Identifier, faction.Identifier == factionIdentifier); + } + } + + public bool LayerExists(Identifier layer) { foreach (MapEntity me in MapEntity.MapEntityList) { - if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this) { continue; } - - var layerAsIdentifier = me.Layer.ToIdentifier(); - if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) - { - me.HiddenInGame = factionIdentifier != layerAsIdentifier; -#if CLIENT - //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that - if (me.HiddenInGame && me is Item item) - { - foreach (var lightComponent in item.GetComponents()) - { - lightComponent.Light.Enabled = false; - } - } -#endif - } + if (me.Submarine == this || me.Layer == layer) { return true; } } + return false; + } + + public void SetLayerEnabled(Identifier layer, bool enabled, bool sendNetworkEvent = false) + { + foreach (MapEntity me in MapEntity.MapEntityList) + { + if (string.IsNullOrEmpty(me.Layer) || me.Submarine != this || me.Layer != layer) { continue; } + me.HiddenInGame = !enabled; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif + } +#if SERVER + if (sendNetworkEvent) + { + GameMain.Server.CreateEntityEvent(this, new SetLayerEnabledEventData(layer, enabled)); + } +#endif } public void Update(float deltaTime) @@ -1230,6 +1247,8 @@ namespace Barotrauma public void NeutralizeBallast() { + if (PhysicsBody.BodyType != BodyType.Dynamic) { return; } + float neutralBallastLevel = 0.5f; int selectedSteeringValue = 0; foreach (Item item in Item.ItemList) @@ -1240,8 +1259,8 @@ namespace Barotrauma //find how many pumps/engines in this sub the steering item is connected to int steeringValue = 1; - Connection connectionX = item.GetComponent()?.Connections.Find(c => c.Name == "velocity_x_out"); - Connection connectionY = item.GetComponent()?.Connections.Find(c => c.Name == "velocity_y_out"); + Connection connectionX = item.GetComponent()?.Connections.Find(static c => c.Name == "velocity_x_out"); + Connection connectionY = item.GetComponent()?.Connections.Find(static c => c.Name == "velocity_y_out"); if (connectionX != null) { foreach (Engine engine in steering.Item.GetConnectedComponentsRecursive(connectionX)) @@ -1371,7 +1390,7 @@ namespace Barotrauma } /// - /// Returns true if the sub is same as the other. + /// Returns true if the sub is same as the other, or connected to it via docking ports. /// public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub); @@ -1459,22 +1478,39 @@ namespace Barotrauma public static Rectangle GetBorders(XElement submarineElement) { - Vector4 bounds = Vector4.Zero; + Vector4 bounds = new Vector4(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue); foreach (XElement element in submarineElement.Elements()) { - if (element.Name != "Structure") { continue; } + if (element.Name == "Structure") + { + string name = element.GetAttributeString("name", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + StructurePrefab prefab = Structure.FindPrefab(name, identifier); + if (prefab == null || !prefab.Body) { continue; } - string name = element.GetAttributeString("name", ""); - Identifier identifier = element.GetAttributeIdentifier("identifier", ""); - StructurePrefab prefab = Structure.FindPrefab(name, identifier); - if (prefab == null || !prefab.Body) { continue; } + var rect = element.GetAttributeRect("rect", Rectangle.Empty); + bounds = new Vector4( + Math.Min(rect.X, bounds.X), + Math.Max(rect.Y, bounds.Y), + Math.Max(rect.Right, bounds.Z), + Math.Min(rect.Y - rect.Height, bounds.W)); + } + else if (element.Name == "LinkedSubmarine") + { + Point dimensions = element.GetAttributePoint("dimensions", Point.Zero); + Point pos = element.GetAttributeVector2("pos", Vector2.Zero).ToPoint(); + bounds = new Vector4( + Math.Min(pos.X - dimensions.X / 2, bounds.X), + Math.Max(pos.Y + dimensions.Y / 2, bounds.Y), + Math.Max(pos.X + dimensions.X / 2, bounds.Z), + Math.Min(pos.Y - dimensions.Y / 2, bounds.W)); + } + } - var rect = element.GetAttributeRect("rect", Rectangle.Empty); - bounds = new Vector4( - Math.Min(rect.X, bounds.X), - Math.Max(rect.Y, bounds.Y), - Math.Max(rect.Right, bounds.Z), - Math.Min(rect.Y - rect.Height, bounds.W)); + if (bounds.X == float.MaxValue || bounds.Y == float.MinValue || bounds.Z == float.MinValue || bounds.W == float.MaxValue) + { + //no bounds found + return Rectangle.Empty; } return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W)); @@ -1667,6 +1703,11 @@ namespace Barotrauma } } + foreach (Identifier layer in Info.LayersHiddenByDefault) + { + SetLayerEnabled(layer, enabled: false); + } + GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged.Register(upgradeEventIdentifier, _ => ResetCrushDepth()); #if CLIENT @@ -1721,6 +1762,22 @@ namespace Barotrauma realWorldCrushDepth = null; } + /// + /// Normally crush depth is determined by the crush depths of the walls and upgrades applied on them. + /// This method forces the crush depths of all the walls to the specified value. + /// + /// + /// Depth in "real world" units (meters from the surface of Europa, the value you see on the nav terminal). + public void SetCrushDepth(float realWorldCrushDepth) + { + foreach (Structure structure in Structure.WallList) + { + if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; } + structure.CrushDepth = realWorldCrushDepth; + } + this.realWorldCrushDepth = realWorldCrushDepth; + } + public static void RepositionEntities(Vector2 moveAmount, IEnumerable entities) { if (moveAmount.LengthSquared() < 0.00001f) { return; } @@ -1777,6 +1834,10 @@ namespace Barotrauma element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString())); element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); + if (Info.LayersHiddenByDefault.Any()) + { + element.Add(new XAttribute("layerhiddenbydefault", string.Join(", ", Info.LayersHiddenByDefault))); + } if (Info.Type == SubmarineType.OutpostModule) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index c4948fc15..d15858f41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -504,7 +504,10 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { - if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) { continue; } + if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine != CanEnterSubmarine.True) + { + continue; + } foreach (Limb limb in c.AnimController.Limbs) { @@ -567,7 +570,7 @@ namespace Barotrauma { buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay); } - return new Vector2(0.0f, buoyancy * Body.Mass * 10.0f) * massRatio; + return new Vector2(0.0f, buoyancy * totalMass * 10.0f) * massRatio; } public void ApplyForce(Vector2 force) @@ -684,9 +687,26 @@ namespace Barotrauma private bool CheckCharacterCollision(Contact contact, Character character) { - //characters that can't enter the sub always collide regardless of gaps - if (!character.AnimController.CanEnterSubmarine) { return true; } if (character.Submarine != null) { return false; } + switch (character.AnimController.CanEnterSubmarine) + { + case CanEnterSubmarine.False: + //characters that can't enter the sub always collide regardless of gaps + return true; + case CanEnterSubmarine.Partial: + //characters that can partially enter the sub can poke their limbs inside, but not the collider + if (contact.FixtureB.Body == + character.AnimController.Collider.FarseerBody) + { + return true; + } + if (contact.FixtureB.Body.UserData is Limb limb && + !limb.Params.CanEnterSubmarine) + { + return true; + } + break; + } contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2 points); @@ -717,17 +737,23 @@ namespace Barotrauma if (adjacentGap == null) { return true; } } - if (newHull != null) - { - CoroutineManager.Invoke(() => - { - if (character != null && !character.Removed) - { - character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true); - } - }); + if (character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial) + { + return contact.FixtureB.Body == character.AnimController.Collider.FarseerBody; + } + else + { + if (newHull != null) + { + CoroutineManager.Invoke(() => + { + if (character != null && !character.Removed) + { + character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true); + } + }); + } } - return false; } @@ -880,17 +906,12 @@ namespace Barotrauma } } -#if CLIENT - int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); - for (int i = 0; i < particleAmount; i++) - { - GameMain.ParticleManager.CreateParticle("iceshards", - ConvertUnits.ToDisplayUnits(impact.ImpactPos) + Rand.Vector(Rand.Range(1.0f, 50.0f)), - Rand.Vector(Rand.Range(50.0f, 500.0f)) + impact.Velocity); - } -#endif + HandleLevelCollisionProjSpecific(impact); } + + partial void HandleLevelCollisionProjSpecific(Impact impact); + private void HandleSubCollision(Impact impact, Submarine otherSub) { Debug.Assert(otherSub != submarine); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index a71c4f3fb..8d0102c32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -1,4 +1,4 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -234,6 +234,11 @@ namespace Barotrauma public readonly Dictionary> OutpostNPCs = new Dictionary>(); + /// + /// Names of layers that get automatically hidden when loading the sub + /// + public HashSet LayersHiddenByDefault { get; private set; } = new HashSet(); + //constructors & generation ---------------------------------------------------- public SubmarineInfo() { @@ -319,6 +324,7 @@ namespace Barotrauma IsManuallyOutfitted = original.IsManuallyOutfitted; Tags = original.Tags; OutpostGenerationParams = original.OutpostGenerationParams; + LayersHiddenByDefault = original.LayersHiddenByDefault; if (original.OutpostModuleInfo != null) { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); @@ -385,6 +391,12 @@ namespace Barotrauma RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier()); + + foreach (Identifier hiddenLayer in SubmarineElement.GetAttributeIdentifierArray("layerhiddenbydefault", Array.Empty())) + { + LayersHiddenByDefault.Add(hiddenLayer); + } + // Backwards compatibility if (recommendedCrewExperience == "Beginner") { @@ -793,6 +805,13 @@ namespace Barotrauma characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); float price = Price; + + // Adjust by campaign difficulty settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + price *= campaign.Settings.ShipyardPriceMultiplier; + } + if (characterList.Any()) { if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index a8a0551db..90ad144f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -1060,7 +1060,8 @@ namespace Barotrauma Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element)) { - spawnType = spawnType + spawnType = spawnType, + Layer = element.GetAttributeString(nameof(Layer), null) }; string idCardDescString = element.GetAttributeString("idcarddesc", ""); @@ -1115,7 +1116,8 @@ namespace Barotrauma element.Add(new XAttribute("ID", ID), new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)), new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)), - new XAttribute("spawn", spawnType)); + new XAttribute("spawn", spawnType), + new XAttribute(nameof(Layer), Layer ?? string.Empty)); if (SpawnType == SpawnType.ExitPoint) { element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WreckConverter.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WreckConverter.cs new file mode 100644 index 000000000..8d1a6e711 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WreckConverter.cs @@ -0,0 +1,193 @@ +#nullable enable + +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + static class WreckConverter + { + private static readonly string[] itemsToRemove = + { + "circuitboxcomponent", + "wire", + }; + + public static XElement ConvertToWreck(XElement submarineElement) + { + ImmutableHashSet availableWreckContainerTags = ItemPrefab.Prefabs + .SelectMany(ip => ip.PreferredContainers.SelectMany(pc => pc.Primary.Union(pc.Secondary))) + .Where(t => !ItemPrefab.Prefabs.ContainsKey(t) && t.StartsWith("wreck")) + .ToImmutableHashSet(); + + bool monsterSpawnPointCreated = false; + + List warnings = new List(); + + var wreckElement = new XElement(submarineElement); + foreach (var element in wreckElement.Elements().ToList()) + { + var identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) + { + if (element.NameAsIdentifier() == "waypoint") + { + if (element.GetAttributeEnum("spawn", SpawnType.Path) != SpawnType.Human) { continue; } + if (element.GetAttributeIdentifier("job", Identifier.Empty) == Identifier.Empty) + { + element.SetAttributeValue("spawn", SpawnType.Enemy); + DebugConsole.NewMessage("Converted a non-job-specific spawnpoint to an enemy spawnpoint."); + monsterSpawnPointCreated = true; + } + else + { + element.SetAttributeValue("spawn", SpawnType.Corpse); + } + } + continue; + } + + //remove if set to be removed + var tags = element.GetAttributeIdentifierArray("tags", Array.Empty()); + if (itemsToRemove.Any(it => tags.Contains(it.ToIdentifier()))) + { + element.Remove(); + continue; + } + + bool tagsModified = false; + for (int i = 0; i < tags.Length; i++) + { + Identifier wreckTag = ("wreck" + tags[i]).ToIdentifier(); + if (availableWreckContainerTags.Contains(wreckTag)) + { + DebugConsole.NewMessage($"Replaced tag {tags[i]} with {wreckTag} in item \"{identifier}\"."); + tags[i] = wreckTag; + tagsModified = true; + } + } + if (tagsModified) + { + element.SetAttributeValue("tags", string.Join(",", tags.Select(t => t.ToString()))); + } + + Identifier[] wreckedIdentifiers = + { + (identifier + "wrecked").ToIdentifier(), + (identifier + "_wrecked").ToIdentifier(), + }; + + //turn to wrecked version if one is available + foreach (var wreckedIdentifier in wreckedIdentifiers) + { + var wreckedPrefab = MapEntityPrefab.FindByIdentifier(wreckedIdentifier); + if (wreckedPrefab == null) { continue; } + + var oldPrefab = MapEntityPrefab.FindByIdentifier(identifier); + element.SetAttributeValue("identifier", wreckedIdentifier); + float currentScale = element.GetAttributeFloat("scale", oldPrefab.Scale); + element.SetAttributeValue("scale", currentScale * (wreckedPrefab.Scale / oldPrefab.Scale)); + + if (wreckedPrefab is ItemPrefab wreckedItemPrefab) + { + //remove connections that don't exist in the wreck version + var originalConnectionPanelElement = element.GetChildElement(nameof(ConnectionPanel)); + var wreckedConnectionPanelElement = wreckedItemPrefab.ConfigElement.GetChildElement(nameof(ConnectionPanel)); + if (originalConnectionPanelElement != null && wreckedConnectionPanelElement != null) + { + foreach (var connectionElement in originalConnectionPanelElement.Elements().ToList()) + { + var elementName = connectionElement.NameAsIdentifier(); + if (elementName != "input" && elementName != "output") { continue; } + string connectionName = connectionElement.GetAttributeString("name", string.Empty); + if (wreckedConnectionPanelElement + .GetChildElements(connectionElement.Name.LocalName) + .None(c => c.GetAttributeString("name", string.Empty) == connectionName)) + { + connectionElement.Remove(); + } + } + } + } + else if (wreckedPrefab is StructurePrefab wreckedStructurePrefab) + { + //if the dimensions of the structures are different, rescale + //ignore small differences, they tend to be just irrelevant differences in how the sourcerect is scaled + const int MaximumSizeDifference = 5; + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); + if (!wreckedStructurePrefab.ResizeHorizontal) + { + if (Math.Abs(wreckedStructurePrefab.ScaledSize.X - rect.Width) > MaximumSizeDifference) + { + DebugConsole.NewMessage($"The prefab {wreckedStructurePrefab.Name} has different dimensions than the original one. Changing the width from {rect.Width} to {(int)wreckedStructurePrefab.ScaledSize.X}.", Color.Yellow); + } + rect.Width = (int)wreckedStructurePrefab.ScaledSize.X; + } + if (!wreckedStructurePrefab.ResizeVertical) + { + if (Math.Abs(wreckedStructurePrefab.ScaledSize.Y - rect.Height) > MaximumSizeDifference) + { + DebugConsole.NewMessage($"The prefab {wreckedStructurePrefab.Name} has different dimensions than the original one. Changing the height from {rect.Height} to {(int)wreckedStructurePrefab.ScaledSize.Y}.", Color.Yellow); + } + rect.Height = (int)wreckedStructurePrefab.ScaledSize.Y; + } + element.SetAttributeValue("rect", XMLExtensions.RectToString(rect)); + } + break; + } + + var itemContainerElement = element.GetChildElement(nameof(ItemContainer)); + if (itemContainerElement != null) + { + string containedString = itemContainerElement.GetAttributeString("contained", ""); + string[] itemIdStrings = containedString.Split(','); + var itemIds = new HashSet(); + foreach (string idListStr in itemIdStrings) + { + foreach (string idStr in idListStr.Split(';')) + { + if (int.TryParse(idStr, out int id)) { itemIds.Add((UInt16)id); } + } + } + if (itemIds.Any()) + { + List containedItemNames = new List(); + foreach (var itemElement in wreckElement.Elements()) + { + var id = itemElement.GetAttributeUInt16("id", Entity.NullEntityID); + if (itemIds.Contains(id)) + { + containedItemNames.Add(itemElement.GetAttributeString("identifier", string.Empty)); + } + } + warnings.Add($"Potential issue in container \"{identifier}\". The following items are pre-placed, and may interfere with the loot generated in the wreck: " + string.Join(", ", containedItemNames)); + } + } + + //set to 0 condition if repairable, exclude doors and hatches + if (element.GetChildElement(nameof(Repairable)) != null && element.GetChildElement(nameof(Door)) == null) + { + element.SetAttributeValue("conditionpercentage", 0.0f); + } + } + + foreach (var warning in warnings) + { + DebugConsole.AddWarning(warning); + } + + if (!monsterSpawnPointCreated) + { + DebugConsole.ThrowError("There are no monster spawnpoints in the wreck. Remember to add some for monsters to spawn properly!"); + } + + return wreckElement; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index b00e6dc38..2c40216f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -32,6 +32,12 @@ namespace Barotrauma.Networking public const float SpeakRange = 2000.0f; + /// + /// This is shorter than the text chat speak range, because the voice chat is still intelligible (just quiet) close to the maximum range, + /// while the text chat (which drops letters from the message) becomes unintelligible sooner + /// + public const float SpeakRangeVOIP = 1000.0f; + private static readonly string dateTimeFormatLongTimePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; public static Color[] MessageColor = diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index e2fab1f74..9d2fbc195 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -162,7 +162,8 @@ namespace Barotrauma { typeof(AccountId), new ReadWriteBehavior(ReadAccountId, WriteAccountId) }, { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) }, - { typeof(SerializableDateTime), new ReadWriteBehavior(ReadSerializableDateTime, WriteSerializableDateTime) } + { typeof(SerializableDateTime), new ReadWriteBehavior(ReadSerializableDateTime, WriteSerializableDateTime) }, + { typeof(NetLimitedString), new ReadWriteBehavior(ReadNetLString, WriteNetLString) } }; private static readonly ImmutableDictionary, Func> BehaviorFactories = new Dictionary, Func> @@ -458,6 +459,12 @@ namespace Barotrauma private static double ReadDouble(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadDouble(); private static void WriteDouble(double b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteDouble(b); } + // We do not validate that the string read is within the max length, but do we need to? + // Modified client could send a network message with a really long string when we use NetLimitedString + // but they could also just do that for any other network message. + private static NetLimitedString ReadNetLString(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => new NetLimitedString(inc.ReadString()); + private static void WriteNetLString(NetLimitedString b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteString(b.Value); } + private static string ReadString(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadString(); private static void WriteString(string b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteString(b); } @@ -685,6 +692,7 @@ namespace Barotrauma /// float
/// double
/// string
+ ///
///
///
///
diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 0dba2ed92..6630bb4cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -214,15 +214,7 @@ namespace Barotrauma.Networking AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType, textColor: textColor)); } - public virtual void AddChatMessage(ChatMessage message) - { - if (string.IsNullOrEmpty(message.Text)) { return; } - - if (message.Sender != null && !message.Sender.IsDead) - { - message.Sender.ShowSpeechBubble(2.0f, message.Color); - } - } + public abstract void AddChatMessage(ChatMessage message); public static string ClientLogName(Client client, string name = null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index fd647fcab..f7c5235c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -31,7 +31,9 @@ namespace Barotrauma.Networking ///
public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, - order?.GetChatMessage(targetCharacter?.Name, (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + order?.GetChatMessage(targetCharacter?.Name, + (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, + givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), targetCharacter, sender, isNewOrder) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 2ba983261..99cbc056e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -68,6 +68,7 @@ namespace Barotrauma.Networking { public string ServerName; public ImmutableArray ContentPackages; + public bool AllowModDownloads; } [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index fbeadc001..2fda650f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -14,7 +14,14 @@ namespace Barotrauma.Networking /// /// How much skills drop towards the job's default skill levels when dying /// - public static float SkillLossPercentageOnDeath => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnDeath ?? 50.0f; + public static float SkillLossPercentageOnDeath => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnDeath ?? 20.0f; + + /// + /// How much more the skills drop towards the job's default skill levels + /// when dying, in addition to SkillLossPercentageOnDeath, if the player + /// chooses to respawn in the middle of the round + /// + public static float SkillLossPercentageOnImmediateRespawn => GameMain.NetworkMember?.ServerSettings?.SkillLossPercentageOnImmediateRespawn ?? 10.0f; public enum State { @@ -76,6 +83,10 @@ namespace Barotrauma.Networking private float updateReturnTimer; + public bool CanRespawnAgain => + /*can never respawn again if we're currently transporting and transport time is set to be infinite*/ + !(CurrentState == State.Transporting && maxTransportTime <= 0.0f); + public Submarine RespawnShuttle { get; private set; } public RespawnManager(NetworkMember networkMember, SubmarineInfo shuttleInfo) @@ -88,7 +99,7 @@ namespace Barotrauma.Networking RespawnShuttle = new Submarine(shuttleInfo, true); RespawnShuttle.PhysicsBody.FarseerBody.OnCollision += OnShuttleCollision; //set crush depth slightly deeper than the main sub's - RespawnShuttle.RealWorldCrushDepth = Math.Max(RespawnShuttle.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth * 1.2f); + RespawnShuttle.SetCrushDepth(Math.Max(RespawnShuttle.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth * 1.2f)); //prevent wifi components from communicating between the respawn shuttle and other subs List wifiComponents = new List(); @@ -356,25 +367,6 @@ namespace Barotrauma.Networking RespawnCharactersProjSpecific(shuttlePos); } - public static AfflictionPrefab GetRespawnPenaltyAfflictionPrefab() - { - return AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); - } - - public static Affliction GetRespawnPenaltyAffliction() - { - return GetRespawnPenaltyAfflictionPrefab()?.Instantiate(10.0f); - } - - public static void GiveRespawnPenaltyAffliction(Character character) - { - var respawnPenaltyAffliction = GetRespawnPenaltyAffliction(); - if (respawnPenaltyAffliction != null) - { - character.CharacterHealth.ApplyAffliction(targetLimb: null, respawnPenaltyAffliction); - } - } - public Vector2 FindSpawnPos() { if (Level.Loaded == null || Submarine.MainSub == null) { return Vector2.Zero; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 76f3671b7..8526f2f49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -50,8 +50,6 @@ namespace Barotrauma.Networking public enum NetFlags : byte { None = 0x0, - Name = 0x1, - Message = 0x2, Properties = 0x4, Misc = 0x8, LevelSeed = 0x10, @@ -322,24 +320,26 @@ namespace Barotrauma.Networking } } - private string serverName; + private string serverName = string.Empty; + + [Serialize("", IsPropertySaveable.Yes)] public string ServerName { get { return serverName; } set { - string val = value; - if (val.Length > NetConfig.ServerNameMaxLength) { val = val.Substring(0, NetConfig.ServerNameMaxLength); } - if (serverName == val) { return; } - serverName = val; + string newName = value; + if (newName.Length > NetConfig.ServerNameMaxLength) { newName = newName.Substring(0, NetConfig.ServerNameMaxLength); } + if (serverName == newName) { return; } + if (newName.IsNullOrWhiteSpace()) { return; } + serverName = newName; ServerDetailsChanged = true; -#if SERVER - UpdateFlag(NetFlags.Name); -#endif } } private string serverMessageText; + + [Serialize("", IsPropertySaveable.Yes)] public string ServerMessageText { get { return serverMessageText; } @@ -348,11 +348,11 @@ namespace Barotrauma.Networking string val = value; if (val.Length > NetConfig.ServerMessageMaxLength) { val = val.Substring(0, NetConfig.ServerMessageMaxLength); } if (serverMessageText == val) { return; } +#if SERVER + GameMain.Server?.SendChatMessage(TextManager.AddPunctuation(':', TextManager.Get("servermotd"), val).Value, ChatMessageType.Server); +#endif serverMessageText = val; ServerDetailsChanged = true; -#if SERVER - UpdateFlag(NetFlags.Message); -#endif } } @@ -437,7 +437,7 @@ namespace Barotrauma.Networking private set; } - [Serialize(50f, IsPropertySaveable.Yes)] + [Serialize(20f, IsPropertySaveable.Yes)] /// /// How much skills drop towards the job's default skill levels when dying /// @@ -447,6 +447,18 @@ namespace Barotrauma.Networking private set; } + [Serialize(10f, IsPropertySaveable.Yes)] + /// + /// How much more the skills drop towards the job's default skill levels + /// when dying, in addition to SkillLossPercentageOnDeath, if the player + /// chooses to respawn in the middle of the round + /// + public float SkillLossPercentageOnImmediateRespawn + { + get; + private set; + } + [Serialize(60.0f, IsPropertySaveable.Yes)] public float AutoRestartInterval { @@ -554,6 +566,7 @@ namespace Barotrauma.Networking } } + [Serialize(false, IsPropertySaveable.Yes)] public bool AutoRestart { get { return autoRestart; } @@ -629,6 +642,7 @@ namespace Barotrauma.Networking set; } + [Serialize(0.0f, IsPropertySaveable.Yes)] public float SelectedLevelDifficulty { get { return selectedLevelDifficulty; } @@ -757,13 +771,18 @@ namespace Barotrauma.Networking if (traitorDangerLevel == clampedValue) { return; } traitorDangerLevel = clampedValue; ServerDetailsChanged = true; +#if CLIENT + GameMain.NetLobbyScreen?.SetTraitorDangerLevel(traitorDangerLevel); +#endif } } + + private int traitorsMinPlayerCount; [Serialize(defaultValue: 1, isSaveable: IsPropertySaveable.Yes)] public int TraitorsMinPlayerCount { - get; - set; + get { return traitorsMinPlayerCount; } + set { traitorsMinPlayerCount = MathHelper.Clamp(value, 1, NetConfig.MaxPlayers); } } [Serialize(defaultValue: 50.0f, isSaveable: IsPropertySaveable.Yes)] @@ -869,7 +888,11 @@ namespace Barotrauma.Networking { karmaEnabled = value; #if CLIENT - if (karmaSettingsBlocker != null) { karmaSettingsBlocker.Visible = !karmaEnabled || karmaPresetDD.SelectedData as string != "custom"; } + if (karmaSettingsList != null) + { + SetElementInteractability(karmaSettingsList.Content, !karmaEnabled || KarmaPreset != "custom"); + } + karmaElements.ForEach(e => e.Visible = karmaEnabled); #endif } } @@ -1039,8 +1062,9 @@ namespace Barotrauma.Networking .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) .ToImmutableArray(); - public void ReadMonsterEnabled(IReadMessage inc) + public bool ReadMonsterEnabled(IReadMessage inc) { + bool changed = false; InitMonstersEnabled(); var monsterNames = ExtractAndSortKeys(MonsterEnabled); uint receivedMonsterCount = inc.ReadVariableUInt32(); @@ -1053,10 +1077,13 @@ namespace Barotrauma.Networking { foreach (Identifier s in monsterNames) { + MonsterEnabled.TryGetValue(s, out bool prevEnabled); MonsterEnabled[s] = inc.ReadBoolean(); + changed |= prevEnabled != MonsterEnabled[s]; } } inc.ReadPadBits(); + return changed; } public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index 69232df45..d04088c0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -1,5 +1,4 @@ using FarseerPhysics.Dynamics; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -15,7 +14,7 @@ namespace Barotrauma public const Category CollisionItemBlocking = Category.Cat6; public const Category CollisionProjectile = Category.Cat7; public const Category CollisionLevel = Category.Cat8; - public const Category CollisionRepair = Category.Cat9; + public const Category CollisionRepairableWall = Category.Cat9; public static float DisplayToRealWorldRatio = 1.0f / 100.0f; @@ -61,7 +60,7 @@ namespace Barotrauma category = CollisionLevel; return true; case "repair": - category = CollisionRepair; + category = CollisionRepairableWall; return true; default: return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 7f9b856b1..8dc5e25ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -248,6 +248,11 @@ namespace Barotrauma get { return ConvertUnits.ToDisplayUnits(FarseerBody.Position); } } + /// + /// Offset of the DrawPosition from the Position (i.e. how much the interpolated draw position is offset from the "actual position"). In display units. + /// + public Vector2 DrawPositionOffset => DrawPosition - Position; + public Vector2 PrevPosition { get { return prevPosition; } @@ -936,6 +941,24 @@ namespace Barotrauma ApplyTorque(FarseerBody.Mass * torque); } } + + /// + /// Wraps the angle so it has "has the same number of revolutions" as this body, i.e. that the angles are at most 180 degrees apart. + /// For example, if the angle of this body was 720, an angle of 5 would get wrapped to 725. + /// + public float WrapAngleToSameNumberOfRevolutions(float angle) + { + if (float.IsInfinity(angle)) { return angle; } + while (Rotation - angle > MathHelper.TwoPi) + { + angle += MathHelper.TwoPi; + } + while (Rotation - angle < -MathHelper.TwoPi) + { + angle -= MathHelper.TwoPi; + } + return angle; + } public void Remove() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 7a6e24432..c4e8b814e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -267,7 +267,7 @@ namespace Voronoi2 { Vector2 dir = Vector2.Normalize(Point1 - Point2); Vector2 normal = new Vector2(dir.Y, -dir.X); - if (cell != null && Vector2.Dot(normal, Vector2.Normalize(Center - cell.Center)) < 0) + if (cell != null && Vector2.Dot(normal, Vector2.Normalize(Center - (cell.Center - cell.Translation))) < 0) { normal = -normal; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 780e23939..106f3e879 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -31,8 +31,8 @@ namespace Barotrauma lastUpdateID++; } #elif CLIENT - levelDifficultyScrollBar.BarScroll = difficulty / 100.0f; - levelDifficultyScrollBar.OnMoved(levelDifficultyScrollBar, levelDifficultyScrollBar.BarScroll); + levelDifficultySlider.BarScroll = difficulty / 100.0f; + levelDifficultySlider.OnMoved(levelDifficultySlider, levelDifficultySlider.BarScroll); #endif } @@ -47,9 +47,6 @@ namespace Barotrauma GameMain.Server.ServerSettings.BotCount = botCount; lastUpdateID++; } -#endif -#if CLIENT - botCountText.Text = botCount.ToString(); #endif } @@ -61,15 +58,6 @@ namespace Barotrauma GameMain.Server.ServerSettings.BotSpawnMode = botSpawnMode; lastUpdateID++; } -#endif -#if CLIENT - - botSpawnModeText.Text = TextManager.Get(botSpawnMode.ToString()); - botSpawnModeText.ToolTip = TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip") + "\n\n" + TextManager.Get("botspawn.campaignnote"); - foreach (var btn in botSpawnModeButtons) - { - btn.ToolTip = botSpawnModeText.ToolTip; - } #endif } @@ -79,10 +67,6 @@ namespace Barotrauma { GameMain.NetworkMember.ServerSettings.TraitorProbability = probability; } -#if CLIENT - traitorProbabilitySlider.BarScroll = probability; - traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll); -#endif } public void SetTraitorDangerLevel(int dangerLevel) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs index 1c26b59df..d32bd3fa8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty/SerializableProperty.cs @@ -209,7 +209,7 @@ namespace Barotrauma PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value)); break; case "identifierarray": - PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value).ToIdentifiers().ToArray()); + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseIdentifierArray(value)); break; } } @@ -218,8 +218,6 @@ namespace Barotrauma DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}", e); return false; } - - return true; } @@ -295,7 +293,7 @@ namespace Barotrauma PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value)); return true; case "identifierarray": - PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value).ToIdentifiers().ToArray()); + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseIdentifierArray((string)value)); return true; default: DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value}"); @@ -1089,7 +1087,7 @@ namespace Barotrauma { case "requireditem": case "requireditems": - itemComponent.requiredItems.Clear(); + itemComponent.RequiredItems.Clear(); itemComponent.DisabledRequiredItems.Clear(); itemComponent.SetRequiredItems(element, allowEmpty: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index b21c9327e..3df639213 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1041,6 +1041,12 @@ namespace Barotrauma public static bool IsOverride(this XElement element) => element.NameAsIdentifier() == "override"; + /// + /// Get the root element of the document, or the first child element of the root if it's an override element. + /// Or in other words, the "rootmost element that actually contains some content". + /// + public static XElement GetRootExcludingOverride(this XDocument doc) => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + public static XElement FirstElement(this XElement element) => element.Elements().FirstOrDefault(); public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 12cad8921..68e231cb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -41,6 +41,13 @@ namespace Barotrauma BossHealthBarsOnly, HideAll } + + public enum InteractionLabelDisplayMode + { + Everything, + InteractionAvailable, + LooseItems + } public static class GameSettings { @@ -67,6 +74,8 @@ namespace Barotrauma RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/", AimAssistAmount = DefaultAimAssist, ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, + ChatSpeechBubbles = true, + InteractionLabelDisplayMode = InteractionLabelDisplayMode.Everything, EnableMouseLook = true, ChatOpen = true, CrewMenuOpen = true, @@ -143,6 +152,8 @@ namespace Barotrauma public float AimAssistAmount; public bool EnableMouseLook; public EnemyHealthBarMode ShowEnemyHealthBars; + public bool ChatSpeechBubbles; + public InteractionLabelDisplayMode InteractionLabelDisplayMode; public bool ChatOpen; public bool CrewMenuOpen; public bool ShowOffensiveServerPrompt; @@ -301,6 +312,7 @@ namespace Barotrauma { InputType.Health, Keys.H }, { InputType.Ragdoll, Keys.Space }, { InputType.Aim, MouseButton.SecondaryMouse }, + { InputType.DropItem, Keys.None }, { InputType.InfoTab, Keys.Tab }, { InputType.Chat, Keys.None }, @@ -314,6 +326,7 @@ namespace Barotrauma { InputType.LocalVoice, Keys.None }, { InputType.ToggleChatMode, Keys.R }, { InputType.Command, MouseButton.MiddleMouse }, + { InputType.ContextualCommand, Keys.LeftShift }, { InputType.PreviousFireMode, MouseButton.MouseWheelDown }, { InputType.NextFireMode, MouseButton.MouseWheelUp }, @@ -332,7 +345,8 @@ namespace Barotrauma { InputType.Use, Keys.E }, { InputType.Select, MouseButton.PrimaryMouse }, { InputType.Deselect, MouseButton.SecondaryMouse }, - { InputType.Shoot, MouseButton.PrimaryMouse } + { InputType.Shoot, MouseButton.PrimaryMouse }, + { InputType.ShowInteractionLabels, Keys.LeftAlt } }.ToImmutableDictionary(); public static KeyMapping GetDefault() => new KeyMapping @@ -382,6 +396,13 @@ namespace Barotrauma { foreach (var savedBinding in savedBindings) { + if (savedBinding.Key is InputType.Run or InputType.TakeHalfFromInventorySlot && + defaultBinding.Key == InputType.ContextualCommand) + { + //run and contextual commands have always defaulted to Shift, but the latter used to be hard-coded. + //don't show a warning about those being bound to the same key + continue; + } if (savedBinding.Value == defaultBinding.Value) { OnGameMainHasLoaded += () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 3c34fd89a..6968ad80f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -99,7 +99,12 @@ namespace Barotrauma /// /// Check against the target's limb type. See . /// - LimbType + LimbType, + + /// + /// Check against the current World Hostility setting (previously known as "Difficulty"). + /// + WorldHostility } public enum LogicalOperatorType @@ -168,6 +173,8 @@ namespace Barotrauma public readonly ImmutableArray AttributeValueAsTags; public readonly float? FloatValue; + private readonly WorldHostilityOption cachedHostilityValue; + /// /// If set to the name of one of the target's ItemComponents, the conditionals defined by this element check against the properties of that component. /// Only works on items. @@ -176,7 +183,7 @@ namespace Barotrauma /// /// If set to true, the conditionals defined by this element check against the attacking character instead of the attacked character. - /// Only applies to a character's attacks. + /// Only applies to a character's attacks and targeting parameters. /// public readonly bool TargetSelf; @@ -287,6 +294,11 @@ namespace Barotrauma { FloatValue = value; } + + if (Type == ConditionType.WorldHostility && Enum.TryParse(AttributeValue, ignoreCase: true, out WorldHostilityOption hostilityValue)) + { + cachedHostilityValue = hostilityValue; + } } public static (ComparisonOperatorType ComparisonOperator, string ConditionStr) ExtractComparisonOperatorFromConditionString(string str) @@ -394,6 +406,11 @@ namespace Barotrauma { return PropertyMatchesRequirement(target, property); } + else if (targetChar?.SerializableProperties != null + && targetChar.SerializableProperties.TryGetValue(AttributeName, out var characterProperty)) + { + return PropertyMatchesRequirement(targetChar, characterProperty); + } return ComparisonOperatorIsNotEquals; case ConditionType.SkillRequirement: if (targetChar != null) @@ -408,30 +425,47 @@ namespace Barotrauma case ConditionType.HasStatusTag: if (target == null) { return ComparisonOperatorIsNotEquals; } - // NOTE: This can be optimized further by returning - // when a match passes with the Equals operator and - // when a match fails with the NotEquals operator. - // The current form has better readability. - int numMatchingEffects = 0; - int numEffectsAffectingTarget = 0; - - foreach (var durationEffect in StatusEffect.DurationList) + int numTagsFound = 0; + foreach (var tag in AttributeValueAsTags) { - if (!durationEffect.Targets.Contains(target)) { continue; } - numEffectsAffectingTarget++; - if (StatusEffectMatchesTagCondition(durationEffect.Parent)) { numMatchingEffects++; } + bool tagFound = false; + foreach (var durationEffect in StatusEffect.DurationList) + { + if (!durationEffect.Targets.Contains(target)) { continue; } + if (durationEffect.Parent.HasTag(tag)) + { + tagFound = true; + break; + } + } + if (!tagFound) + { + foreach (var delayedEffect in DelayedEffect.DelayList) + { + if (!delayedEffect.Targets.Contains(target)) { continue; } + if (delayedEffect.Parent.HasTag(tag)) + { + tagFound = true; + break; + } + } + } + if (tagFound) + { + numTagsFound++; + } } - - foreach (var delayedEffect in DelayedEffect.DelayList) - { - if (!delayedEffect.Targets.Contains(target)) { continue; } - numEffectsAffectingTarget++; - if (StatusEffectMatchesTagCondition(delayedEffect.Parent)) { numMatchingEffects++; } - } - return ComparisonOperatorIsNotEquals - ? numMatchingEffects >= numEffectsAffectingTarget // true when none of the effects have any of the tags - : numMatchingEffects > 0; // true when any effects have all tags + ? numTagsFound < AttributeValueAsTags.Length // true when some tag wasn't found + : numTagsFound >= AttributeValueAsTags.Length; // true when all the tags are found + case ConditionType.WorldHostility: + { + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + return Compare(campaign.Settings.WorldHostility, cachedHostilityValue, ComparisonOperator); + } + return false; + } default: bool equals = CheckOnlyEquality(target); return ComparisonOperatorIsNotEquals @@ -538,16 +572,6 @@ namespace Barotrauma return SufficientTagMatches(matches); } - private bool StatusEffectMatchesTagCondition(StatusEffect statusEffect) - { - int matches = 0; - foreach (var tag in AttributeValueAsTags) - { - if (statusEffect.HasTag(tag)) { matches++; } - } - return SufficientTagMatches(matches); - } - private bool NumberMatchesRequirement(float testedValue) { if (!FloatValue.HasValue) { return ComparisonOperatorIsNotEquals; } @@ -624,5 +648,17 @@ namespace Barotrauma } } + public static bool Compare(T leftValue, T rightValue, ComparisonOperatorType comparisonOperator) where T : IComparable + { + return comparisonOperator switch + { + ComparisonOperatorType.NotEquals => leftValue.CompareTo(rightValue) != 0, + ComparisonOperatorType.GreaterThan => leftValue.CompareTo(rightValue) > 0, + ComparisonOperatorType.LessThan => leftValue.CompareTo(rightValue) < 0, + ComparisonOperatorType.GreaterThanEquals => leftValue.CompareTo(rightValue) >= 0, + ComparisonOperatorType.LessThanEquals => leftValue.CompareTo(rightValue) <= 0, + _ => leftValue.CompareTo(rightValue) == 0, + }; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 01b05f328..9a9788259 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -132,11 +132,11 @@ namespace Barotrauma public enum SpawnPositionType { /// - /// The position of the StatusEffect's target. + /// The position of the entity (item, character, limb) the StatusEffect is defined in. /// This, /// - /// The inventory of the StatusEffect's target. + /// The inventory of the entity (item, character, limb) the StatusEffect is defined in. /// ThisInventory, /// @@ -146,18 +146,26 @@ namespace Barotrauma /// /// The inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory) /// - ContainedInventory + ContainedInventory, + /// + /// The position of the entity the StatusEffect is targeting. If there are multiple targets, an item is spawned at all of them. + /// + Target } public enum SpawnRotationType { /// - /// Fixed rotation specified using the Rotation attribute. + /// Neutral (0) rotation. Can be rotated further using the Rotation attribute. /// - Fixed, + None, /// /// The rotation of the entity executing the StatusEffect /// + This, + /// + /// The rotation from the position of the spawned entity to the target of the StatusEffect + /// Target, /// /// The rotation of the limb executing the StatusEffect, or the limb the StatusEffect is targeting @@ -270,8 +278,16 @@ namespace Barotrauma Equip = element.GetAttributeBool("equip", false); SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); - RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); + if (element.GetAttributeString("rotationtype", string.Empty).Equals("Fixed", StringComparison.OrdinalIgnoreCase)) + { + //backwards compatibility, "This" was previously (inaccurately) called "Fixed" + RotationType = SpawnRotationType.This; + } + else + { + RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.This : SpawnRotationType.Target); + } InheritEventTags = element.GetAttributeBool(nameof(InheritEventTags), false); } } @@ -331,12 +347,23 @@ namespace Barotrauma /// Should the talents that trigger when the character gains skills be triggered by the effect? /// public readonly bool TriggerTalents; + /// + /// Should the amount be multiplied by delta time? Useful if you want to give a skill increase per frame. + /// + public readonly bool UseDeltaTime; + /// + /// Should the amount be inversely proportional to the current skill level? + /// Meaning, the higher the skill level, the less the skill is increased. + /// + public readonly bool Proportional; public GiveSkill(ContentXElement element, string parentDebugName) { SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty); Amount = element.GetAttributeFloat(nameof(Amount), 0); TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true); + UseDeltaTime = element.GetAttributeBool(nameof(UseDeltaTime), false); + Proportional = element.GetAttributeBool(nameof(Proportional), false); if (SkillIdentifier == Identifier.Empty) { @@ -692,7 +719,7 @@ namespace Barotrauma /// In other words, when enabled, the strength of the affliction(s) caused by this effect is higher on higher-vitality characters. /// Can be used to make characters take the same relative amount of damage regardless of their maximum vitality. /// - private readonly bool? multiplyAfflictionsByMaxVitality; + private readonly bool multiplyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters { @@ -706,6 +733,11 @@ namespace Barotrauma private readonly List giveSkills; private readonly List<(string, ContentXElement)> luaHook; + + private HashSet<(Character targetCharacter, AnimLoadInfo anim)> failedAnimations; + public readonly record struct AnimLoadInfo(AnimationType Type, Either File, float Priority, ImmutableArray ExpectedSpeciesNames); + private readonly List animationsToTrigger; + /// /// How long the effect runs (in seconds). Note that if is true, /// there can be multiple instances of the effect running at a time. @@ -806,6 +838,11 @@ namespace Barotrauma targetTypes |= targetType; } } + if (targetTypes == 0) + { + string errorMessage = $"Potential error in StatusEffect ({parentDebugName}). Target not defined, the effect might not work correctly. Use target=\"This\" if you want the effect to target the entity it's defined in. Setting \"This\" as the target."; + DebugConsole.AddSafeError(errorMessage); + } var targetIdentifiers = element.GetAttributeIdentifierArray(Array.Empty(), "targetnames", "targets", "targetidentifiers", "targettags"); if (targetIdentifiers.Any()) @@ -816,15 +853,8 @@ namespace Barotrauma triggeredEventTargetTag = element.GetAttributeIdentifier("eventtargettag", triggeredEventTargetTag); triggeredEventEntityTag = element.GetAttributeIdentifier("evententitytag", triggeredEventEntityTag); triggeredEventUserTag = element.GetAttributeIdentifier("eventusertag", triggeredEventUserTag); - spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); - - var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality)); - if (multiplyAfflictionsElement != null) - { - multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false); - } - + multiplyAfflictionsByMaxVitality = element.GetAttributeBool(nameof(multiplyAfflictionsByMaxVitality), false); #if CLIENT playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false); #endif @@ -993,11 +1023,13 @@ namespace Barotrauma continue; } } - - Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", "strength")); - afflictionInstance.Probability = subElement.GetAttributeFloat(1.0f, "probability"); + + Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", nameof(afflictionInstance.Strength))); + // Deserializing the object normally might cause some unexpected side effects. At least it clamps the strength of the affliction, which we don't want here. + // Could probably be solved by using the NonClampedStrength or by bypassing the clamping, but ran out of time and played it safe here. + afflictionInstance.Probability = subElement.GetAttributeFloat(1.0f, nameof(afflictionInstance.Probability)); + afflictionInstance.MultiplyByMaxVitality = subElement.GetAttributeBool(nameof(afflictionInstance.MultiplyByMaxVitality), false); Afflictions.Add(afflictionInstance); - break; case "reduceaffliction": if (subElement.GetAttribute("name") != null) @@ -1088,6 +1120,24 @@ namespace Barotrauma case "hook": luaHook ??= new List<(string, ContentXElement)>(); luaHook.Add((subElement.GetAttributeString("name", ""), subElement)); + case "triggeranimation": + AnimationType animType = subElement.GetAttributeEnum("type", def: AnimationType.NotDefined); + string fileName = subElement.GetAttributeString("filename", def: null) ?? subElement.GetAttributeString("file", def: null); + Either file = fileName != null ? fileName.ToLowerInvariant() : subElement.GetAttributeContentPath("path"); + if (!file.TryGet(out string _)) + { + if (!file.TryGet(out ContentPath _) || (file.TryGet(out ContentPath contentPath) && contentPath.IsNullOrWhiteSpace())) + { + DebugConsole.ThrowError($"Error in a element of {subElement.ParseContentPathFromUri()}: neither path nor filename defined!"); + } + } + else + { + float priority = subElement.GetAttributeFloat("priority", def: 0f); + Identifier[] expectedSpeciesNames = subElement.GetAttributeIdentifierArray("expectedspecies", Array.Empty()); + animationsToTrigger ??= new List(); + animationsToTrigger.Add(new AnimLoadInfo(animType, file, priority, expectedSpeciesNames.ToImmutableArray())); + } break; } } @@ -1187,6 +1237,8 @@ namespace Barotrauma { foreach (Powered powered in Powered.PoweredList) { + //make sure we didn't already add this item due to it having some other Powered component + if (targets.Contains(powered)) { continue; } Item item = powered.Item; if (!item.Removed && CheckDistance(item) && IsValidTarget(item)) { @@ -1557,16 +1609,14 @@ namespace Barotrauma { var result = GameMain.LuaCs.Hook.Call("statusEffect.apply." + item.Prefab.Identifier, this, deltaTime, entity, targets, worldPosition); - if (result != null && result.Value) - return; + if (result != null && result.Value) { return; } } if (entity is Character character) { var result = GameMain.LuaCs.Hook.Call("statusEffect.apply." + character.SpeciesName, this, deltaTime, entity, targets, worldPosition); - if (result != null && result.Value) - return; + if (result != null && result.Value) { return; } } } @@ -1580,6 +1630,8 @@ namespace Barotrauma } } + Item parentItem = entity as Item; + PhysicsBody parentItemBody = parentItem?.body; Hull hull = GetHull(entity); Vector2 position = GetPosition(entity, targets, worldPosition); if (useItemCount > 0) @@ -1644,7 +1696,28 @@ namespace Barotrauma } } } - + if (removeItem) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } + } + } + if (removeCharacter) + { + for (int i = 0; i < targets.Count; i++) + { + var target = targets[i]; + if (target is Character character) + { + Entity.Spawner?.AddEntityToRemoveQueue(character); + } + else if (target is Limb limb) + { + Entity.Spawner?.AddEntityToRemoveQueue(limb.character); + } + } + } if (breakLimb || hideLimb) { for (int i = 0; i < targets.Count; i++) @@ -1753,8 +1826,8 @@ namespace Barotrauma RegisterTreatmentResults(user, entity as Item, limb, affliction, result); } } - - foreach (var (affliction, amount) in ReduceAffliction) + + foreach ((Identifier affliction, float amount) in ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; @@ -1775,11 +1848,11 @@ namespace Barotrauma float prevVitality = targetCharacter.Vitality; if (targetLimb != null) { - targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, attacker: user, treatmentAction: actionType); } else { - targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, attacker: user, treatmentAction: actionType); } if (!targetCharacter.IsDead) { @@ -1842,6 +1915,8 @@ namespace Barotrauma } } } + + TryTriggerAnimation(target, entity); if (isNotClient) { @@ -1860,13 +1935,23 @@ namespace Barotrauma if (giveSkills != null) { - foreach (GiveSkill giveSkill in giveSkills) + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter is { Removed: false }) { - Character targetCharacter = CharacterFromTarget(target); - if (targetCharacter != null && !targetCharacter.Removed) + foreach (GiveSkill giveSkill in giveSkills) { Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; - targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount, !giveSkill.TriggerTalents); + float amount = giveSkill.UseDeltaTime ? giveSkill.Amount * deltaTime : giveSkill.Amount; + + if (giveSkill.Proportional) + { + targetCharacter.Info?.ApplySkillGain(skillIdentifier, amount, !giveSkill.TriggerTalents); + } + else + { + targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, amount, !giveSkill.TriggerTalents); + } + Identifier GetRandomSkill() { return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty; @@ -1924,10 +2009,9 @@ namespace Barotrauma { foreach (EventPrefab eventPrefab in triggeredEvents) { - Event ev = eventPrefab.CreateInstance(); + Event ev = eventPrefab.CreateInstance(eventManager.RandomSeed); if (ev == null) { continue; } - eventManager.QueuedEvents.Enqueue(ev); - + eventManager.QueuedEvents.Enqueue(ev); if (ev is ScriptedEvent scriptedEvent) { if (!triggeredEventTargetTag.IsEmpty) @@ -2021,11 +2105,11 @@ namespace Barotrauma } } if (i == characterSpawnInfo.Count) // Only perform the below actions if this is the last character being spawned. - { + { if (characterSpawnInfo.TransferControl) { #if CLIENT - if (Character.Controlled == target) + if (Character.Controlled == target) { Character.Controlled = newCharacter; } @@ -2036,7 +2120,7 @@ namespace Barotrauma GameMain.Server.SetClientCharacter(c, newCharacter); } #endif - } + } if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } } } @@ -2055,13 +2139,17 @@ namespace Barotrauma } } - if (spawnItems != null) + if (spawnItems != null && spawnItems.Count > 0) { if (spawnItemRandomly) { if (spawnItems.Count > 0) { - SpawnItem(spawnItems.GetRandomUnsynced()); + var randomSpawn = spawnItems.GetRandomUnsynced(); + for (int i = 0; i < randomSpawn.Count; i++) + { + ProcessItemSpawnInfo(randomSpawn); + } } } else @@ -2070,269 +2158,33 @@ namespace Barotrauma { for (int i = 0; i < itemSpawnInfo.Count; i++) { - SpawnItem(itemSpawnInfo); + ProcessItemSpawnInfo(itemSpawnInfo); } } } - } - void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) - { - Item parentItem = entity as Item; - if (user == null && parentItem != null) + void ProcessItemSpawnInfo(ItemSpawnInfo spawnInfo) { - // Set the user for projectiles spawned from status effects (e.g. flak shrapnels) - SetUser(parentItem.GetComponent()?.User); - } - switch (chosenItemSpawnInfo.SpawnPosition) - { - case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner?.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => - { - Projectile projectile = newItem.GetComponent(); - if (entity != null) - { - var rope = newItem.GetComponent(); - if (rope != null && sourceBody != null && sourceBody.UserData is Limb sourceLimb) - { - rope.Attach(sourceLimb, newItem); -#if SERVER - newItem.CreateServerEvent(rope); -#endif - } - float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad); - float rotation = chosenItemSpawnInfo.RotationRad; - Vector2 worldPos; - if (sourceBody != null) - { - worldPos = sourceBody.Position; - if (user?.Submarine != null) - { - worldPos += user.Submarine.Position; - } - } - else - { - worldPos = entity.WorldPosition; - } - switch (chosenItemSpawnInfo.RotationType) - { - case ItemSpawnInfo.SpawnRotationType.Fixed: - if (sourceBody != null) - { - rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.RotationRad); - } - else if (parentItem?.body != null) - { - rotation = parentItem.body.TransformRotation(chosenItemSpawnInfo.RotationRad); - } - break; - case ItemSpawnInfo.SpawnRotationType.Target: - rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); - break; - case ItemSpawnInfo.SpawnRotationType.Limb: - if (sourceBody != null) - { - rotation = sourceBody.TransformedRotation; - } - break; - case ItemSpawnInfo.SpawnRotationType.Collider: - if (parentItem?.body != null) - { - rotation = parentItem.body.Rotation; - } - else if (user != null) - { - rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2; - } - break; - case ItemSpawnInfo.SpawnRotationType.MainLimb: - if (user != null) - { - rotation = user.AnimController.MainLimb.body.TransformedRotation; - } - break; - case ItemSpawnInfo.SpawnRotationType.Random: - if (projectile != null) - { - DebugConsole.LogError("Random rotation is not supported for Projectiles."); - } - else - { - rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced); - } - break; - default: - throw new NotImplementedException("Item spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); - } - if (user != null) - { - rotation += chosenItemSpawnInfo.RotationRad * user.AnimController.Dir; - } - rotation += spread; - if (projectile != null) - { - var sourceEntity = (sourceBody?.UserData as ISpatialEntity) ?? entity; - Vector2 spawnPos = sourceEntity.SimPosition; - projectile.Shoot(user, spawnPos, spawnPos, rotation, - ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); - projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine; - } - else if (newItem.body != null) - { - newItem.body.SetTransform(newItem.SimPosition, rotation); - Vector2 impulseDir = new Vector2(MathF.Cos(rotation), MathF.Sin(rotation)); - newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse); - } - } - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - break; - case ItemSpawnInfo.SpawnPositionType.ThisInventory: - { - Inventory inventory = null; - if (entity is Character character && character.Inventory != null) - { - inventory = character.Inventory; - } - else if (entity is Item item) - { - foreach (ItemContainer itemContainer in item.GetComponents()) - { - if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) - { - inventory = itemContainer?.Inventory; - break; - } - } - if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null) - { - return; - } - } - if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => - { - if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) - { - //if the item is both pickable and wearable, try to wear it instead of picking it up - List allowedSlots = - item.GetComponents().Count() > 1 ? - new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : - new List(item.AllowedSlots); - allowedSlots.Remove(InvSlotType.Any); - character.Inventory.TryPutItem(item, null, allowedSlots); - } - OnItemSpawned(item, chosenItemSpawnInfo); - }); - } - } - break; - case ItemSpawnInfo.SpawnPositionType.SameInventory: - { - Inventory inventory = null; - if (entity is Character character) - { - inventory = character.Inventory; - } - else if (entity is Item item) - { - inventory = item.ParentInventory; - } - if (inventory != null) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => - { - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - } - else if (chosenItemSpawnInfo.SpawnIfNotInInventory) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position, onSpawned: (Item newItem) => - { - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - } - } - break; - case ItemSpawnInfo.SpawnPositionType.ContainedInventory: - { - Inventory thisInventory = null; - if (entity is Character character) - { - thisInventory = character.Inventory; - } - else if (entity is Item item) - { - var itemContainer = item.GetComponent(); - thisInventory = itemContainer?.Inventory; - if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) - { - return; - } - } - if (thisInventory != null) - { - foreach (Item item in thisInventory.AllItems) - { - Inventory containedInventory = item.GetComponent()?.Inventory; - if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) - { - Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => - { - OnItemSpawned(newItem, chosenItemSpawnInfo); - }); - break; - } - } - } - } - break; - } - void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo) - { - newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition; - if (itemSpawnInfo.InheritEventTags) + if (spawnInfo.SpawnPosition == ItemSpawnInfo.SpawnPositionType.Target) { - foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + foreach (var target in targets) { - if (activeEvent is ScriptedEvent scriptedEvent) + if (target is Entity targetEntity) { - scriptedEvent.InheritTags(entity, newItem); + SpawnItem(spawnInfo, entity, sourceBody, position, targetEntity); } } } + else + { + SpawnItem(spawnInfo, entity, sourceBody, position, targetEntity: null); + } } } } ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); - //do this last - the entities spawned by the effect might need the entity for something, so better to remove it last - if (removeItem) - { - for (int i = 0; i < targets.Count; i++) - { - if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } - } - } - if (removeCharacter) - { - for (int i = 0; i < targets.Count; i++) - { - var target = targets[i]; - if (target is Character character) - { - Entity.Spawner?.AddEntityToRemoveQueue(character); - } - else if (target is Limb limb) - { - Entity.Spawner?.AddEntityToRemoveQueue(limb.character); - } - } - } - if (oneShot) { Disabled = true; @@ -2343,17 +2195,283 @@ namespace Barotrauma intervalTimers[entity] = Interval; } - static Character CharacterFromTarget(ISerializableEntity target) + } + private static Character CharacterFromTarget(ISerializableEntity target) + { + Character targetCharacter = target as Character; + if (targetCharacter == null) { - Character targetCharacter = target as Character; - if (targetCharacter == null) + if (target is Limb targetLimb && !targetLimb.Removed) { - if (target is Limb targetLimb && !targetLimb.Removed) + targetCharacter = targetLimb.character; + } + } + return targetCharacter; + } + + void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo, Entity entity, PhysicsBody sourceBody, Vector2 position, Entity targetEntity) + { + Item parentItem = entity as Item; + PhysicsBody parentItemBody = parentItem?.body; + if (user == null && parentItem != null) + { + // Set the user for projectiles spawned from status effects (e.g. flak shrapnels) + SetUser(parentItem.GetComponent()?.User); + } + + if (chosenItemSpawnInfo.SpawnPosition == ItemSpawnInfo.SpawnPositionType.Target && targetEntity != null) + { + entity = targetEntity; + position = entity.WorldPosition; + if (entity is Item it) + { + sourceBody ??= + (entity as Item)?.body ?? + (entity as Character)?.AnimController.Collider; + } + } + + switch (chosenItemSpawnInfo.SpawnPosition) + { + case ItemSpawnInfo.SpawnPositionType.This: + case ItemSpawnInfo.SpawnPositionType.Target: + Entity.Spawner?.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { - targetCharacter = targetLimb.character; + Projectile projectile = newItem.GetComponent(); + if (entity != null) + { + var rope = newItem.GetComponent(); + if (rope != null && sourceBody != null && sourceBody.UserData is Limb sourceLimb) + { + rope.Attach(sourceLimb, newItem); +#if SERVER + newItem.CreateServerEvent(rope); +#endif + } + float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad); + float rotation = chosenItemSpawnInfo.RotationRad; + Vector2 worldPos; + if (sourceBody != null) + { + worldPos = sourceBody.Position; + if (user?.Submarine != null) + { + worldPos += user.Submarine.Position; + } + } + else + { + worldPos = entity.WorldPosition; + } + switch (chosenItemSpawnInfo.RotationType) + { + case ItemSpawnInfo.SpawnRotationType.None: + rotation = chosenItemSpawnInfo.RotationRad; + break; + case ItemSpawnInfo.SpawnRotationType.This: + if (sourceBody != null) + { + rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.RotationRad); + } + else if (parentItemBody != null) + { + rotation = parentItemBody.TransformRotation(chosenItemSpawnInfo.RotationRad); + } + break; + case ItemSpawnInfo.SpawnRotationType.Target: + rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); + break; + case ItemSpawnInfo.SpawnRotationType.Limb: + if (sourceBody != null) + { + rotation = sourceBody.TransformedRotation; + } + break; + case ItemSpawnInfo.SpawnRotationType.Collider: + if (parentItemBody != null) + { + rotation = parentItemBody.TransformedRotation; + } + else if (user != null) + { + rotation = user.AnimController.Collider.Rotation + MathHelper.PiOver2; + } + break; + case ItemSpawnInfo.SpawnRotationType.MainLimb: + if (user != null) + { + rotation = user.AnimController.MainLimb.body.TransformedRotation; + } + break; + case ItemSpawnInfo.SpawnRotationType.Random: + if (projectile != null) + { + DebugConsole.LogError("Random rotation is not supported for Projectiles."); + } + else + { + rotation = Rand.Range(0f, MathHelper.TwoPi, Rand.RandSync.Unsynced); + } + break; + default: + throw new NotImplementedException("Item spawn rotation type not implemented: " + chosenItemSpawnInfo.RotationType); + } + if (user != null) + { + rotation += chosenItemSpawnInfo.RotationRad * user.AnimController.Dir; + } + rotation += spread; + if (projectile != null) + { + var sourceEntity = (sourceBody?.UserData as ISpatialEntity) ?? entity; + Vector2 spawnPos = sourceEntity.SimPosition; + projectile.Shoot(user, spawnPos, spawnPos, rotation, + ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + projectile.Item.Submarine = projectile.LaunchSub = sourceEntity?.Submarine; + } + else if (newItem.body != null) + { + newItem.body.SetTransform(newItem.SimPosition, rotation); + Vector2 impulseDir = new Vector2(MathF.Cos(rotation), MathF.Sin(rotation)); + newItem.body.ApplyLinearImpulse(impulseDir * chosenItemSpawnInfo.Impulse); + } + } + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + break; + case ItemSpawnInfo.SpawnPositionType.ThisInventory: + { + Inventory inventory = null; + if (entity is Character character && character.Inventory != null) + { + inventory = character.Inventory; + } + else if (entity is Item item) + { + foreach (ItemContainer itemContainer in item.GetComponents()) + { + if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + inventory = itemContainer?.Inventory; + break; + } + } + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null) + { + return; + } + } + if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => + { + if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) + { + //if the item is both pickable and wearable, try to wear it instead of picking it up + List allowedSlots = + item.GetComponents().Count() > 1 ? + new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : + new List(item.AllowedSlots); + allowedSlots.Remove(InvSlotType.Any); + character.Inventory.TryPutItem(item, null, allowedSlots); + } + OnItemSpawned(item, chosenItemSpawnInfo); + }); + } + } + break; + case ItemSpawnInfo.SpawnPositionType.SameInventory: + { + Inventory inventory = null; + if (entity is Character character) + { + inventory = character.Inventory; + } + else if (entity is Item item) + { + inventory = item.ParentInventory; + } + if (inventory != null) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + } + else if (chosenItemSpawnInfo.SpawnIfNotInInventory) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + } + } + break; + case ItemSpawnInfo.SpawnPositionType.ContainedInventory: + { + Inventory thisInventory = null; + if (entity is Character character) + { + thisInventory = character.Inventory; + } + else if (entity is Item item) + { + var itemContainer = item.GetComponent(); + thisInventory = itemContainer?.Inventory; + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + return; + } + } + if (thisInventory != null) + { + foreach (Item item in thisInventory.AllItems) + { + Inventory containedInventory = item.GetComponent()?.Inventory; + if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) + { + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + OnItemSpawned(newItem, chosenItemSpawnInfo); + }); + break; + } + } + } + } + break; + } + void OnItemSpawned(Item newItem, ItemSpawnInfo itemSpawnInfo) + { + newItem.Condition = newItem.MaxCondition * itemSpawnInfo.Condition; + if (itemSpawnInfo.InheritEventTags) + { + foreach (var activeEvent in GameMain.GameSession.EventManager.ActiveEvents) + { + if (activeEvent is ScriptedEvent scriptedEvent) + { + scriptedEvent.InheritTags(entity, newItem); + } + } + } + } + } + + private void TryTriggerAnimation(ISerializableEntity target, Entity entity) + { + if (animationsToTrigger == null) { return; } + // Could probably use a similar pattern in other places above too, but refactoring statuseffects is very volatile. + if ((CharacterFromTarget(target) ?? entity as Character) is Character targetCharacter) + { + foreach (AnimLoadInfo animLoadInfo in animationsToTrigger) + { + if (failedAnimations != null && failedAnimations.Contains((targetCharacter, animLoadInfo))) { continue; } + if (!targetCharacter.AnimController.TryLoadTemporaryAnimation(animLoadInfo, throwErrors: animLoadInfo.ExpectedSpeciesNames.Contains(targetCharacter.SpeciesName))) + { + failedAnimations ??= new HashSet<(Character, AnimLoadInfo)>(); + failedAnimations.Add((targetCharacter, animLoadInfo)); } } - return targetCharacter; } } @@ -2452,7 +2570,7 @@ namespace Barotrauma } } - foreach (var (affliction, amount) in element.Parent.ReduceAffliction) + foreach ((Identifier affliction, float amount) in element.Parent.ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; @@ -2473,11 +2591,11 @@ namespace Barotrauma float prevVitality = targetCharacter.Vitality; if (targetLimb != null) { - targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType, attacker: element.User); } else { - targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType, attacker: element.User); } if (!targetCharacter.IsDead) { @@ -2493,6 +2611,8 @@ namespace Barotrauma } } } + + element.Parent.TryTriggerAnimation(target, element.Entity); } element.Parent.ApplyProjSpecific(deltaTime, @@ -2530,10 +2650,10 @@ namespace Barotrauma return afflictionMultiplier * AfflictionMultiplier; } - private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) + private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool multiplyByMaxVitality) { float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); - if (multiplyByMaxVitality ?? affliction.MultiplyByMaxVitality) + if (multiplyByMaxVitality) { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs index 830c43168..2af009b39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Tags.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Tags.cs @@ -51,6 +51,15 @@ public static class Tags public static readonly Identifier ArtifactHolder = "artifactholder".ToIdentifier(); public static readonly Identifier Thalamus = "thalamus".ToIdentifier(); + public static readonly Identifier IgnoreThis = "ignorethis".ToIdentifier(); + public static readonly Identifier UnignoreThis = "unignorethis".ToIdentifier(); + + public static readonly Identifier DeconstructThis = "deconstructthis".ToIdentifier(); + public static readonly Identifier DontDeconstructThis = "dontdeconstructthis".ToIdentifier(); + + public static readonly Identifier Poison = "poison".ToIdentifier(); + public static readonly Identifier Stun = "stun".ToIdentifier(); + public static readonly Identifier Crate = "crate".ToIdentifier(); public static readonly Identifier DontSellItems = "dontsellitems".ToIdentifier(); public static readonly Identifier CargoContainer = "cargocontainer".ToIdentifier(); @@ -67,6 +76,8 @@ public static class Tags public static readonly Identifier StunnerItem = "stunner".ToIdentifier(); public static readonly Identifier MobileRadio = "mobileradio".ToIdentifier(); + public static readonly Identifier Scooter = "scooter".ToIdentifier(); + /// /// Any handcuffs. /// @@ -110,5 +121,8 @@ public static class Tags public static readonly Identifier ElectricalSkill = "electrical".ToIdentifier(); public static readonly Identifier MechanicalSkill = "mechanical".ToIdentifier(); public static readonly Identifier MedicalSkill = "medical".ToIdentifier(); + + public static readonly Identifier SkillLossDeathResistance = "skilllossdeath".ToIdentifier(); + public static readonly Identifier SkillLossRespawnResistance = "skilllossrespawn".ToIdentifier(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index f8b5849f3..61be54ef6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -172,7 +172,7 @@ namespace Barotrauma var allTexts = TextPacks[GameSettings.CurrentConfig.Language] .SelectMany(p => p.Texts.TryGetValue(tag, out var value) ? (IEnumerable)value - : Array.Empty()); + : Array.Empty()).ToList(); var firstOverride = allTexts.FirstOrDefault(t => t.IsOverride); if (firstOverride != default) @@ -190,17 +190,25 @@ namespace Barotrauma var allTexts = TextPacks[GameSettings.CurrentConfig.Language] .SelectMany(p => p.Texts); - var firstOverride = allTexts.SelectMany(kvp => kvp.Value).FirstOrDefault(t => t.IsOverride); - if (firstOverride != default) + foreach (var textList in allTexts) { - return allTexts - .Where(kvp => kvp.Value.Any(t => t.IsOverride && t.TextPack == firstOverride.TextPack)) - .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v.String))); - } - else - { - return allTexts - .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v.String))); + var firstOverride = textList.Value.FirstOrDefault(t => t.IsOverride); + if (firstOverride != default) + { + //if there's any overrides for this tag, only return the overrides + foreach (var text in textList.Value) + { + if (!text.IsOverride) { continue; } + yield return new KeyValuePair(textList.Key, text.String); + } + } + else + { + foreach (var text in textList.Value) + { + yield return new KeyValuePair(textList.Key, text.String); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs index 99ba1035e..7cecbfa72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorEvent.cs @@ -57,15 +57,14 @@ namespace Barotrauma }; protected override IEnumerable NonActionChildElementNames => nonActionChildElementNames; - public TraitorEvent(TraitorEventPrefab prefab) : base(prefab) + public TraitorEvent(TraitorEventPrefab prefab, int seed) : base(prefab, seed) { this.prefab = prefab; codeWord = string.Empty; } - public override void Init(EventSet? parentSet = null) + protected override void InitEventSpecific(EventSet? parentSet = null) { - base.Init(parentSet); if (traitor == null) { DebugConsole.ThrowError($"Error when initializing event \"{prefab.Identifier}\": traitor not set.\n" + Environment.StackTrace); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 8365930bb..de9e8f723 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -55,6 +55,12 @@ namespace Barotrauma price = location?.GetAdjustedMechanicalCost((int)price) ?? price; + // Adjust by campaign difficulty settings + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + price *= campaign.Settings.ShipyardPriceMultiplier; + } + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characterList.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs new file mode 100644 index 000000000..d522174c2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetLimitedString.cs @@ -0,0 +1,22 @@ +#nullable enable + +namespace Barotrauma +{ + internal readonly struct NetLimitedString + { + public readonly string Value; + public const int MaxLength = 255; + + public static readonly NetLimitedString Empty = new(string.Empty); + + public NetLimitedString(string value) + { + Value = value.Length > MaxLength + ? value[..MaxLength] + : value; + } + + public override string ToString() + => Value; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index ccea18bfa..262d7b87a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -639,6 +639,9 @@ namespace Barotrauma if (o is null) { throw new ArgumentNullException(); } } + /// + /// Converts a percentage value in the 0-1 range to a string representation in the format "x %" according to the grammar rules of the selected language + /// public static string GetFormattedPercentage(float v) { return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value; diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 3178f482c..fa9b4e4be 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,247 @@ +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.4.4.1 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Fixed welding objective completing automatically in the basic tutorial due to the reduced flooding rates. +- Small fixes to localization issues. +- Bots don't consider opiates valid treatments for burns, because the amount of burns they heal now is so low it just leads to wasting meds. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.4.4.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Updated localizations. +- "Lock default wiring" server setting locks components in circuit boxes too. +- Fixed circuit box labels being resizeable to a negative size, making them impossible to select afterwards. +- Fixed inability to remove circuit box labels in multiplayer. + +------------------------------------------------------------------------------------------------------------------------------------------------- +v1.4.3.0 +------------------------------------------------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added new lost cargo missions, in which you must recover cargo from a sunken sub. +- Added six new beacon stations, and updated existing beacon stations. +- Added seven new wrecks. +- Chat messages and NPC dialog is shown in speech bubbles above the character's head. Can be disabled in the game settings. +- Improved the particle effects when the sub hits a level wall, and when cutting/destroying level walls. +- Certain afflictions and talents change the character's walking/running animations (e.g. drunkenness, the "musical talents", vigor, hyperactivity, concussion). +- Removed the randomization of quality from fabricated stackable items. The randomization made them annoying to fabricate, because the items of different quality couldn't be stacked in the fabricator's output slot. +- Motion sensor ranges are visualized when wearing engineer's goggles. +- Made fabricators and deconstructors faster in outposts (it's not fun to force everyone to wait while someone is working a fabricator in an outpost). +- Fire no longer damages husk eggs, fruit and explosives over time. Allowing them to be partially damaged made them really difficult to handle when they happened to be in a stack. Now husk eggs are immune to fire, and the rest get destroyed completely after being in fire for a moment. +- Improvements to the visual effects of lava vents and hydrothermal vents. +- Pets leave behind a corpse when they die. Having them just "pop" often meant you couldn't be sure if the pet had gone missing or died. +- Added a hotkey for dropping the held item. Not bound to any key by default. +- Added a hotkey (by default Alt) that displays a label on all nearby interactable items. The label can be clicked on to interact with the item. Should make it easier to e.g. find loose items on the floor or pick up a specific item when there's many close to each other. +- Tons of changes and improvements to outpost events: the main goal has been to make the things described by the event popups "actually happen", as opposed to just describing them in text. There's now more visual and audio effects in the events, and the things described by the texts tend to actually exist in the game world. There's also been lots of changes to the outcomes of the events: the choices you make should now generally be more meaningful. +- Most outpost events can now be started by another player if the first player who encounters them chooses to ignore them. +- Made the bilge pump circuits non-interactable and the bilge pumps non-wireable in outposts. It was too easy to abuse them to drown the outpost. +- All the pathways from the location you start at in the campaign lead to an outpost. Choosing a new destination at an uninhabited location seems confusing to new players, who haven't yet seen how transitions between levels work. +- Improvements and fixes to harpoons and ropes: the user can now pull towards the target by pressing space (but only when diving), added sounds to reeling and the rope snapping, adjusted the forces and changed how the forces were applied to both the user and the target. +- Added ON_DOCK and ON_UNDOCK outputs to docking ports. +- New automatic docking hatch assembly (much simpler than the old one, now built using a circuit box with labels that explain how it works). +- Added an option to add labels inside circuit boxes (can be used to e.g. explain parts of the circuit). +- Added a menu to the submarine editor that lists all the container tags used in the submarine, explains which tags are available and how they're used, and allows adding them to containers more easily. There's also now a warning on saving if the submarine is missing any common/recommended tags. +- Clown crates now have pressure immunity, making them potentially useful for more than just fooling around. + +Monsters: +- Added Viperling, a venomous variant of Spinelings. +- Reintroduced the legacy monster Mantis. Keep an eye out for anything unusual on the walls when exploring caves! +- Changes to make big monsters more of a threat to characters inside the sub, not just the submarine itself: + - Charybdis can poke its head inside the sub, bite and pull characters out. + - Made monsters better at targeting positions on the hull with a character on the other side (meaning they're more likely to be able to cause shrapnel damage). + - Made the shrapnel particles more noticeable. + - Endworm can poke its mandibles inside the sub and damage characters inside. + - Heavy impacts can launch off very high-velocity shrapnel that can penetrate a couple of inner walls, similar to spineling spikes. + - Added some weaknesses to abyss monsters: Endworm has a weak spot in its mouth, Charybdis flees for a moment if it takes heavy damage to the head or mouth (a feature that initially was there but had been bugged for a while). +- Updated husk's ragdoll, textures and animations. +- Husks can now go unconscious and eventually get back up if not "properly" killed, the same way as huskified humans. +- Adjustments to the loot dropped by Latcher and Charybdis. + +Diving suit changes: +- Several changes to to make it less obvious choice to wear diving suits indoors all the time: + - Hull breaches flood the sub more slowly now, giving you more time to find a suit. + - Reduced walking speed when wearing a suit (with adjustments to the animation to make the suits feel more "tanky", as opposed to just making the character look like it's walking in slow-motion). + - Reduced the damage protection of suits - it was previously so high it encouraged wearing the suits in all situations just for the damage protection they offer. + - Wearing a suit obstructs your vision more now. + - Reduced the crush depths of diving suits to make them match the maximum crush depth of a fully upgraded sub. Allowing players to survive below the sub's crush depth didn't make that much sense: it was practically impossible to recover the sub, so it's better to treat as a "game over" state and kill the players, as opposed to leaving the game in an unrecoverable "soft-locked" state. +- Decreased the armor ratings (damage modifiers) of the (early-game) diving suits. +- Added Explosion Damage resistance to diving suits and protective gear (and clown outfits, ka-honk). +- Made the exosuit more powerful by giving it more damage resistance, a chance to ignore stuns and increasing its speed + +Balance: +- Minor adjustments to the turret balance (most noticeably, made explosive ammo less OP, armor piercing ammo more effective against structures). +- Removed threshers from Cold Caverns. Thresher bites can now cause infected wounds, which can be treated with antibiotic glue, broad-spectrum antibiotics or by applying ethanol or rum to the wound. +- Acid grenades have a slightly longer duration. +- Increased the amount of hyperactivity given by energy drinks. It's still pretty weak, but the previous effect was practically meaningless (making the item more harmful than useful due to the nausea it can cause). +- Dual-wielding ranged weapons reduces reload times and accuracy. + +Medical system: +- Made opiates less of a "solution for everything": they have a much higher risk of causing addiction and overdoses, and morphine heals much more slowly, making it less viable for combat-heavy situations. +- Added "infection" affliction type to add some variety to afflictions. Thresher bites and, on high world hostility campaigns, bleeding wounds and untreated burns have a chance of becoming infected. +- Added "alcohol sensitivity" as a side-effect of antibiotics. This causes drunkenness to build up much faster, so avoid using antibiotics and alcohol together! +- Stabilozine's effects no longer stack: it stops the progress of poisons, but doesn't cure poisonings. +- Added "adrenaline rush" as an effect for adrenaline. "Adrenaline rush" keeps the patient conscious for its duration, removes all active stun when inflicted and applies short-term stun resistance. +- Husk infections can be treated with sufforin and cyanide (but you must be careful to have a cure at hand!). +- Antibiotic glue can be used multiple times. +- Made Pomegrenade Extract a bit more useful: gives the "slow metabolism" buff. +- Ethanol and rum can be poured on limbs to treat infections and burns. +- Removed skill requirements from tonic liquid. +- Alien blood causes organ damage, making it less viable as a risk-free cheap alternative for blood packs. +- Saline can be used to treat infections. +- Resting in bunks heals injuries a little faster now. +- Europabrew can now act as a universal poison cure, but also speeds up husk infection in addition to vulnerability to acid burns. +- Rum can now also be made from pomegrenade. + +Submarines: +- Fixed Azimuth's periscope being too high up, causing the characters to float when using it. +- Fixed hulls being set up strangely in Camel's humps (not extending all the way up to the ceiling, making it possible for there to be holes in the walls without water getting in). +- Various fixes and improvements to the shuttles. +- Fixed too low oxygen output in Typhon's brig, causing characters inside to eventually suffocate without an additional oxygen supply. +- Fixed Azimuth's cargo and engine rooms not draining full due to the hulls extending below the floor and the bilge pumps. +- Fixed sloped wall piece making it difficult to move from Remora to its drone. +- Fixed grenades sometimes going through certain walls (one common spot was Camel's bow). +- Adjusted the hulls in Orca's lower airlock. The small in-between hull with the pump prevented the sub from flooding, because water couldn't flow out from that small hull fast enough to counter the rate of the pump. + +Additional campaign difficulty settings: +- Oxygen tank duration. +- Reactor fuel duration. +- Crew vitality. +- Non-crew NPC vitality. +- Shop purchase prices. +- Shipyard purchase prices (buying new subs and upgrades). +- Severity of injuries from failed repairs. +- Mission income. +- Option to disable the husk infection warning messages. +- Renamed the generic "difficulty" setting as "world hostility", since it only affects things such as monster spawns and environmental hazards. + +Wrecks: +- Added wrecked variants of chaingun, pulse laser, flak cannon, double coilgun and their loaders. +- New wrecks: Barsuk, Camel, Humpback, Typhon 2, Remora, R-29 and Venture. +- Fixes to the wreck spawning logic: linked submarines and non-hulled spaces are taken into account in the placement, preventing them from ending up inside walls. +- Various improvements and fixes to the existing wrecks. +- Added new, higher-res wrecked versions of the shuttle sprites. Marked the old ones as legacy structures. +- Fixed wrecked hatches' broken sprite rendering in front of characters. +- Added console command ‘converttowreck’ to convert submarines to wrecks more easily. + +AI: +- Bots can now be ordered to deconstruct items. There's a separate contextual order for marking items to be deconstructed and a regular order that makes the bots start deconstructing those marked items. +- Improvements to medic AI: they're now better at taking the negative effects of drugs into account, meaning they should be less eager to cause opiate overdoses. +- Made bots better at choosing suits adequate for the current depth. +- Fixed escaped prisoners being unable to seek for weapons in the prisoner transport missions. +- Fixed inability to order bots to turrets that are connected to the periscope via wifi components inside circuit boxes. +- Fixed bots cleaning up active glow sticks and flares. +- Fixed bots sometimes deciding to idle inside docking ports. +- Fixed bots being unable to put two-handed items on their back when trying to use an underwater scooter. +- Fixed medic bots sometimes taking meds from inside their autoinjector headset even if they have suitable meds in a toolbelt or some other container in their inventory. +- Fixed escorted characters (e.g. separatists) attacking you if you steal items from bandits. + +Multiplayer: +- Added player-specific voice chat volume sliders (i.e. if a specific player is very loud in the voice chat, you can reduce their volume or vice versa). +- Fixed inability to remove invisible symbols (= special symbols not included in the fonts the game uses) from your name using the name text box in the server lobby. This seemed to be the cause for the mysterious "name contains symbols disallowed by the server host" errors: if your Steam name contained special symbols the game can't render, you couldn't remove them by editing the name in the server lobby. +- Fixed an exploit that allowed getting free items from stores. +- Replaced Reaper's Tax with optional extra skill loss when respawning mid-round. +- Fixed clients' wallets appearing empty when spawning mid-round. +- Reworked the server lobby layout. The goal was to reorganize the UI to group things in a more logical way and to make things easier to find. +- Adjusted the local voice chat range significantly shorter (comparable to the range of the local text chat now). +- Fixed VOIP breaking after changing the resolution. +- Fixed characters sometimes getting assigned different personality traits between clients and the server. +- Fixed inability to hire more crew if you've reached the maximum crew size, and then fire some to make room for more characters. +- Fixed clients not getting notified in any way when a server has disabled downloading mods directly from the server, causing them to just get stuck in the mod download screen. +- Fixed servers without an active host or anyone else managing the rounds getting stuck if the settings were configured in a specific way: if the respawn transport time was set to infinite (shuttle will spawn once, but will never leave the level and allow a 2nd respawn), the shuttle had spawned and all the players left, and someone new joined the server, they wouldn't be able to end the round because they've never spawned, and could also never respawn. + +Talents: +- Reworked most of the XP-giving talents to make them more meaningful and balanced. +- Fixed "Fireman's Carry" talent not working on stunned characters. +- Fixed tinkerer talent (which allows repairing items above 100%) causing some oddities with repair thresholds: the item would show up as repairable when the condition percentage (relative to the above-normal maximum condition) was below the threshold, but the repair UI wouldn't show up until it was "actually" below the condition where it should become repairable. +- Fixed talent options displaying as locked (gray frame and a lock icon in the corner) even if you've unlocked them if another sub tree is incomplete. +- “Residual Waste" can no longer be exploited for duplicating FPGA circuits. + +Improvements to sub editor's grouping: +- Groups are now called "layers", which reflects their common uses better than "groups". +- Layer options are more easily available in the right-click context menus. +- When a layer is selected, all new entities you place are automatically placed into that layer, so you don't need to manually move every new item you place. +- Added LayerAction as a way for ScriptedEvents to enable or disable layers, useful for modders. + +Fixes: +- Fixed some items sometimes falling through holes on the submarine's floor (and sometimes also getting stuck partially inside the floor). +- Fixed campaign saves getting bricked if you save the game during the brief moment when your crew has died, but the "game over" popup hasn't appeared yet. +- Fixed campaign saves getting bricked if you save the game after you've fired some of your crewmates through the HR manager and the remaining characters have died (or vice versa). +- Fixed (traitor) events that require you to fabricate a specific item complete regardless of what you fabricated. +- Fixed being able to fabricate items from just one ingredient, when the one ingredient matches multiple 'variable' ingredients (ingredients where multiple materials are valid, such as assault rifle magazines which require a munition tip, core and jacket). +- Fixed thalamus organs sometimes spawning in dry hulls, making them die without the player having to do anything. +- Fixed diving suits emitting light when placed in cabinets in the sub editor. +- Fixed waytoascension2 event getting stuck if you choose the "I'll need to think about this" dialog option, end the round, and start a new one. +- Fixed being able to switch submarines using the submarine switch terminals in abandoned outposts or the "dummy outposts" inside normal levels. +- Fixed piezo crystals sometimes spawning close enough to wrecks and beacon stations to zap them. +- Fixed character hover texts (e.g. "[G] grab") not refreshing when you change keybinds. +- Fixed CPR pump animation often causing impact damage to the patient. +- Fixed sonar sometimes not showing parts of the mountain on the ocean floor. +- Fixed beacon stations you've restored during the round not displaying as active on the campaign map until you start a new round. +- Fixed inability to pick up dropped stun batons. +- Wire nodes are placed at the center of the grid cells instead of the corners to get them to align with signal components placed on walls. +- Fixed being able to quickly swap the suit you are currently wearing with a broken suit, by just clicking the broken suit. +- Fixed status effects on certain monster's limbs not working: Charybdis had one that should've made it retreat when it takes heavy damage to the head or mouth, and Fractal Guardian, Moloch and Latcher should've emitted special particles when hit on the weak spots. +- Fixed genetic materials not getting destroyed when a gene splicer gets moved to a duffel bag when a character despawns. +- Fixed language of the units (km/h, m) not changing on the nav terminal when you change the language mid-round. +- Fixed crosshair being mispositioned for a split-second when the character turns around while aiming. +- Fixed leaks sometimes being impossible to repair on "Tail Fin E P2". +- Fixed freezing when you press tab in the sub editor's wiring mode. +- Fixed duct blocks sometimes casting shadows strangely (leaving a see-through gap between the duct block and the adjacent walls). +- Fixed deconstructing a nuclear depth decoy fabricated with the cheap recipe outputting incendium (even though it's not included in the recipe). +- Characters that have thresher genes or who don't need oxygen for some reason (husk infection/symbiosis) don't consume oxygen from tanks. +- Fixed a rare level generation issue that sometimes prevented certain levels from loading, kicking the game back to the main menu or server lobby with the error message "maximum amount of entities exceeded". +- Fixed door's sound muffling effect behaving inconsistently in MP (sometimes muffling sounds even though the door open). +- Fixed certain keys on an AZERTY keyboard not being recognized as keybinds. +- Fixed steep but short wall pieces launching characters downwards too aggressively. +- Fixed buoyancy still affecting docked subs differently than other subs. A sub with docked subs/shuttles would ascend and descend slightly slower than one where the hulls are all part of the same sub. +- Fixed gaps' and waypoints' groups resetting in the sub editor when saving and loading. +- Fixed monsters in the idle (or observing) state moving at too slow speed and changing the direction in which they were heading too rapidly when they wander around. +- Fixed enemy subs' and respawn shuttle's crush depths not being set correctly, sometimes causing them to get crushed in late-campaign levels. +- Fixed items in a character's inventory, who's in the main sub, being counted twice in the "owned item" count displayed in the store interface. +- Fixed purchased items spawning attached to a wall if there's no room in your inventory. +- Fixed characters spawned using console commands not getting ID card tags for the sub they spawn in. + +Modding: +- Fixed inability to publish mods when the language is set to Japanese. +- Added support for defining the ragdoll by a content path, instead of having to define it by a folder. Allows decoupling a ragdoll from a character. +- Added support for (temporarily) overriding character animations using status effects. Allows, for example, wearable items and afflictions to change the character's walking and running animations (see divinggear.xml and afflictions.xml for usage examples). Note that the vanilla animations are configured using the file name, which makes the game search for the animation file from the folder the rest of the character's animations are in. If you want to make your mod add a custom animation to a vanilla character, you need to configure the animation using a file path instead (e.g. path="%ModDir%/CustomWalkAnimation.xml"). +- Fixed variants of character variants not working. +- Added an option to force location types to be owned by a specific faction by adding faction="somefaction" or secondaryfaction="somefaction" to the location type config. +- Fixed inability to drag and drop items into hidden inventories. That can't be done with any vanilla items, but turns out some mods had functionality that relied on this behavior, and we broke it in the latest update. +- Sub editor now warns about rooms with insufficient oxygen output (= if the vents output too little oxygen to support one character). +- Made the joint limit widgets in the character editor (hopefully) a bit more intuitive and easy to use. There's now a gray line that indicates the angle of the limb, and that gray line is what gets clamped between the joint limits. +- Fixed widgets and indicators jittering in the character editor when the character moves. +- The "pitch slide" of turret and ranged weapon charge sounds (used in the vanilla chaingun and rapid fissile accelerator) is now editable using the “ChargeSoundWindupPitchSlide” attribute. +- The deattach speed of repair tools can be edited ("DeattachSpeed" attribute). +- Fixed events that are not allowed at start not triggering if you're in the abyss (or somewhere else a long way away from the destination). +- Fixed PerCave/PerRuin/PerWreck event settings not working correctly in event child sets (the game created the correct number of events, e.g. 3 if there's 3 caves, but each event happened in a random cave). +- Added an option to make TagAction require the target to be in a module with specific tags +- Fixed NPCOperateItemAction being hard-coded to select Controller components, meaning you could only use it to make the character interact with an item that has a Controller component. +- EventManager now works also in the editor test modes. +- Fixed the “interval” attribute of status effects not working when the effect was defined in limbs. +- Added an option to select a specific wreck and a beacon station in the level editor (simplifying testing when you want to test a specific one). +- Fixed room name selection (accessible in the sub editor when editing a hull) being empty if any text file in a mod includes overrides. +- Fixed reputation bars always showing -100 and 100 as the minimum and maximum, even if a custom faction has a different min/max value. +- Fixed crashing when spawning a humanoid character that doesn't have any head sprite configured for it. +- Fixed crashing when placing a door item that's set to be open by default. +- Fixed OnOpen and OnClose action types only working for playing sounds when a door opens or closes, but not for actual status effects. +- Fixed "custom skills" the character's gained during the round (skills levels the character/job originally doesn't have) disappearing between rounds, and only reappearing when the character gains some skill level. +- Fixed MissionActions that are set to choose from a pool of missions always choosing the same mission. +- Fixed inability to take specific kinds of items from a container that requires holding some item. For example, if you needed to hold a crowbar to access a cabinet, and took an item from the cabinet in a way that forces that crowbar item to be unequipped. +- Fixed WeldedSprites not appearing in the sprite editor. +- Fixed connection panel interface's borders overlapping with the connector sprites on certain resolutions if the panel is very tall. +- Fixed OnImpact effects not working on broken items even if AllowWhenBroken is set true. +- Fixed TriggerComponents no longer applying forces or other effects to characters when they ragdoll. +- Made OnSuccess and OnUse effects usable in Repairables (previously only OnFailure effects worked). +- Fixed event editor crashing if you tried to load an event that had action with attributes set to incorrect types of values (e.g. with a CheckMoneyAction with amount set to a string). +- Fixed "Collider" spawn rotation type when spawning items via a status effect not taking the flipping of the parent item into account, causing e.g. projectiles to launch to the right when aiming to the left. +- Fixed tracer particles not showing up when a projectile is spawned using a status effect indoors. +- Fixed crash if a non-attachable item is made attachable mid-round (e.g. via status effects or console commands) and the submarine then saved. +- Fixed monsters always aiming ranged attacks at the position of the target character regardless of which limb they're targeting. Was not noticeable in the vanilla game, because none of the vanilla enemies tried to aim at a specific limb. +- RequireAimToUse is no longer forcibly enabled on ranged weapons (i.e. it's possible to create a ranged weapon you can fire just by left clicking). + ------------------------------------------------------------------------------------------------------------------------------------------------- v1.3.0.4 ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml index b7611ed1b..6eb27946a 100644 --- a/Barotrauma/BarotraumaShared/hintmanager.xml +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -79,4 +79,8 @@ + + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs index 128ead59e..b1376d11f 100644 --- a/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs +++ b/Barotrauma/BarotraumaTest/FabricatorQualityRollTests.cs @@ -36,7 +36,7 @@ public sealed class FabricatorQualityRollTests } } - var result = new Fabricator.QualityResult(startingQuality, plusOneProbability, plusTwoProbability); + var result = new Fabricator.QualityResult(startingQuality, HasRandomQuality: true, plusOneProbability, plusTwoProbability); // iterate to confirm that the percentage chance is correct const int iterations = 100000; diff --git a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs index ece129035..24ae767ff 100644 --- a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs +++ b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using Xunit; @@ -14,8 +14,8 @@ public sealed class GenericToolBoxTests { public static Arbitrary IdentifierPairGenerator() { - return Arb.From(from Identifier first in Arb.Generate() - from Identifier second in Arb.Generate().Where(second => second != first) + return Arb.From(from Identifier first in Arb.Generate().Where(first => !first.Value.Contains('~')) + from Identifier second in Arb.Generate().Where(second => second != first && !second.Value.Contains('~')) select new DifferentIdentifierPair(first, second)); } } @@ -28,6 +28,9 @@ public sealed class GenericToolBoxTests public DifferentIdentifierPair(Identifier first, Identifier second) { if (first == second) { throw new InvalidOperationException("Identifiers must be different"); } + //tildes have a special meaning in stat identifiers, don't use them + if (first.Value.Contains('~')) { throw new InvalidOperationException($"{first} is not a valid identifier."); } + if (second.Value.Contains('~')) { throw new InvalidOperationException($"{second} is not a valid identifier."); } First = first; Second = second; diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs index b7c65fa79..4825d7c0e 100644 --- a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/ReflectionUtils.cs @@ -175,5 +175,54 @@ namespace Barotrauma result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>"; return result; } + + /// + /// Gets a type by its name, with backwards compatibility for types that have been renamed. + /// + /// + public static Type? GetTypeWithBackwardsCompatibility(string nameSpace, string typeName, bool throwOnError, bool ignoreCase) + { + if (Assembly.GetEntryAssembly() is not { } entryAssembly) { return null; } + var types = entryAssembly + .GetTypes() + .Where(t => NameMatches(t.Namespace, nameSpace, ignoreCase)); + + foreach (Type type in types) + { + if (NameMatches(type.Name, typeName, ignoreCase)) + { + return type; + } + + if (type.GetCustomAttribute() is { } knownAsAttribute) + { + if (NameMatches(knownAsAttribute.PreviousName, typeName, ignoreCase)) + { + return type; + } + } + } + + if (throwOnError) + { + throw new TypeLoadException($"Could not find the type {typeName} in namespace {nameSpace}"); + } + + return null; + + static bool NameMatches(string? name1, string? name2, bool ignoreCase) + => string.Equals(name1, name2, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// The names of generic types include the arity at the end (fancy way of saying the number of parameters, e.g. GUISelectionCarousel would be GUISelectionCarousel`1) + /// This method strips that part out. + /// + public static Identifier GetTypeNameWithoutGenericArity(Type type) + { + string name = type.Name; + int index = name.IndexOf('`'); + return (index == -1 ? name : name.Substring(0, index)).ToIdentifier(); + } } } diff --git a/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TypePreviouslyKnownAs.cs b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TypePreviouslyKnownAs.cs new file mode 100644 index 000000000..f94514105 --- /dev/null +++ b/Libraries/BarotraumaLibs/BarotraumaCore/Utils/TypePreviouslyKnownAs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Barotrauma +{ + /// + /// This attribute is used to indicate that a class was previously known by a different name. + /// This is used for backwards compatibility when we have types that are loaded from XML using reflection. + /// + /// Only works in cases where we use to load the type. + /// + /// If you wish to use this, you will need to replace the call to Type.GetType() in the load method with + /// ReflectionUtils.GetTypeWithBackwardsCompatibility(). + /// + [AttributeUsage(AttributeTargets.Class)] + public class TypePreviouslyKnownAs : Attribute + { + public string PreviousName { get; } + + public TypePreviouslyKnownAs(string previousName) + { + PreviousName = previousName; + } + } +} \ No newline at end of file diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs index c5baac2cd..530a6b5de 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Input/KeyboardUtil.SDL.cs @@ -151,6 +151,14 @@ namespace Microsoft.Xna.Framework.Input _map.Add(1073742097, Keys.BrowserRefresh); _map.Add(1073742098, Keys.BrowserFavorites); _map.Add(1073742106, Keys.Sleep); + // Map keys on an Azerty layout to the corresponding keys on a US layout + _map.Add(178, Keys.OemTilde); // ² + _map.Add(41, Keys.OemMinus); // ) + _map.Add(36, Keys.Add); // $ + _map.Add(249, Keys.OemQuotes); // ù + _map.Add(42, Keys.OemPipe); // * + _map.Add(58, Keys.OemPeriod); // : + _map.Add(33, Keys.OemQuestion); // ! } public static Keys ToXna(int key)